This is a combination of a two-part article on Spring Roo and Testing I originally posted on rimple.com, where I write about Spring Roo topics periodically around my book, Spring Roo in Action., co-authored with InfoQ’s Srini Penchikala..
You may find more information about writing Roo add-ons from my recent ETE / Richmond JUG presentation, Leaping Forward with Roo add-ons.
How to test Roo add-ons
I’m working on updates to several Roo add-ons, which I am going to be pushing out to the Silly Weasel Roo repository soon. Here are some challenges and how I overcame them.
Our examples work against the Silly Weasel Coffeescript add-on, which is an extension of the one I started in the book. Over time, we’ll be ramping up the features of that add-on to support the major features of the Maven Plug-In itself.
Testing the Roo Command Service
First off, we need to expose a Roo Command Service, which explains which commands the add-on is exposing to the Roo shell. This service will have methods marked with @CliAvailabilityIndicator and @CliCommand, which tell when a command is exposed, and what method to run when the command is executed, respectively.
Here is our Command object:
package org.sillyweasel.addons.coffeescript; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.Service; import org.springframework.roo.shell.CliAvailabilityIndicator; import org.springframework.roo.shell.CliCommand; import org.springframework.roo.shell.CliOption; import org.springframework.roo.shell.CommandMarker; @Component
@Service
public class CoffeescriptCommands implements CommandMarker { /** * Get a reference to the CoffeescriptOperations from the underlying OSGi * container */ @Reference CoffeescriptOperations operations; @CliAvailabilityIndicator({"coffeescript setup"}) public boolean isSetupCommandAvailable() { return operations.isSetupCommandAvailable(); } @CliAvailabilityIndicator({"coffeescript remove", "coffeescript addjoinset", "coffeescript removejoinset", "coffeescript listjoinsets"}) public boolean isCoffeescriptInstalled() { return operations.isPluginInstalled(); } @CliCommand(value = "coffeescript setup", help = "Install the CoffeeScript compiler") public void setup( @CliOption(key = "outputDirectory", mandatory = false, unspecifiedDefaultValue = "${project.build.directory}") String outputDirectory, @CliOption(key = "coffeeDir", mandatory = false, unspecifiedDefaultValue = "src/main/webapp/scripts", specifiedDefaultValue = "src/main/webapp/scripts") String coffeeDir, @CliOption(key = "bare", mandatory = false, unspecifiedDefaultValue = "false", specifiedDefaultValue = "true") boolean bare) { operations.setup(coffeeDir, outputDirectory, bare); } public void addJoinSet( @CliOption(key = "joinSetId", mandatory = true, specifiedDefaultValue = "main") String joinSetId, @CliOption(key = "includes", mandatory = true, help = "comma-separated list to include in search path") String includes, @CliOption(key = "excludes", mandatory = true, help = "comma-separated list to exclude from search path") String excludes) { // TODO - set up the command } @CliCommand(value = "coffeescript remove", help = "Remove the coffeescript compiler") public void remove() { operations.remove(); } }
Some of this I haven’t written yet (hence the TODOs) but the rest is fleshed out and I’ll be going over it later.
Task #1 – Roo doesn’t include JUnit for add-ons? Let’s fix that…
Right now, no. (ROO-3161) However, you can just add it yerself. I’m using Mockito for mocking (more about that later) as well as messing around with those lovely Hamcrest matchers:
junit junit 4.10 test org.hamcrest hamcrest-all 1.1 test org.mockito mockito-all 1.9.0 test /dependency>
Task #2 – You need to widen the visibility of the Roo-injected OSGi objects to mock them
Because we want to _unit_ test our add-ons without firing up an OSGi container, we will need to mock things. I’m using Mockito, an excellent mock and spy library. But I can’t get to the injected objects, and the test I’m going to show here has to make a “mockery” of the ProjectOperations Roo service. It is defined this way in the code of the Operations implementation class:
private @Reference ProjectOperations projectOperations;
The problem with this is that I can’t just use Mockito’s mocking method to access and fake it out. So, I widened to friendly scope, which allows access from the same package. I guess the better way would maybe have been to create a getter but then I’m exposing it in a wider range than just the add-on’s specific package:
@Reference ProjectOperations projectOperations;
Task #3 – how to test the Command Marker
Ok, so the first thing I want to do is make sure my Command Marker calls the proper command implementation methods. First up, we widen the reference to our CoffeescriptOperations variable:
@Reference CoffeescriptOperations operations;
Then, I’ll write a method that uses Mockito to mock calls to the operations object. This is the test class:
package org.sillyweasel.addons.coffeescript; import junit.framework.Assert; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.Mockito.*; public class CoffeescriptCommandsTest { private CoffeescriptCommands commands; @Before
public void setUp() { commands = new CoffeescriptCommands(); commands.operations = mock(CoffeescriptOperations.class); } @Test public void testIsPluginInstalled() { when(commands.operations.isPluginInstalled()).thenReturn(false); assertThat(commands.isCoffeescriptInstalled(), is(false)); } @Test public void testIsCoffeescriptSetupAvailable() { when(commands.operations.isSetupCommandAvailable()).thenReturn(true); assertThat(commands.isSetupCommandAvailable(), is(true)); } @Test public void testIsCoffeescriptRemoveAvailable() { when(commands.operations.isPluginInstalled()).thenReturn(true); assertThat(commands.isCoffeescriptInstalled(), is(true)); verify(commands.operations, times(1)).isPluginInstalled(); } @Test public void testInstallCoffeeScript() { commands.setup("foo", "bar", true); verify(commands.operations).setup("foo", "bar", true); } @Test public void testRemoveCoffeeScript() { commands.remove(); } @Test @Ignore public void testFileSets() { Assert.fail(); } }
See how I’m using some Hamcrest matchers in there? I like assertThat(…, is()) syntax.
So now we know that our command marker is actually calling the delegate methods when it is being invoked. And we’re on our way, able to mock anything we want to, provided we widen the scope a bit from a 100% internal private member variable.
Testing XML Configuration
Now, I’ll show you how to test the actual XML configuration tasks themselves.
The CoffeescriptOperationsImpl class – under test!
We just showed you how to test the CoffeescriptCommands
object, which delegates calls to the CoffeescriptOperations
OSGi bean, which is implemented by the CoffeescriptOperationsImpl
class. This is where the add-on’s work is being done. So, let’s test it.
Setting up the test class and Mockito
Like last time, we need to configure Mockito. We’ll assume you’ve read up on that post and have installed the proper dependencies.
We need to test that our operations code works, and that it calls the proper Roo services. So, let’s create our bean under test, and then mock the collaborator:
public class CoffeescriptOperationsImplTest { private CoffeescriptOperationsImpl coffeescriptOperations; @Before public void setUp() { coffeescriptOperations = new CoffeescriptOperationsImpl(); coffeescriptOperations.projectOperations = Mockito.mock(ProjectOperations.class); } ...
ROO “ADVANCED ADD-ONS” AND CONFIGURATION
Let’s assume we’re not PERFECT coders. Let’s even assume that we aren’t the best XML developers. I’m pretty bright but make some absolutely silly mistakes during coding, so I’m shining a bright line at myself here.
With the CoffeeScript add-on, we want to manipulate the pom.xml
file – something we don’t need a container to do. Roo uses that good ole’ built-in JAXP library (and Apache implementation of course). As Ben Alex would say, “stock standard Java.” So, we should be able to easily unit test it.
Again, we manually create our class under test, and configure our mocks, in keeping with typical unit tests of components. I had to widen the visibility of the projectOperations
reference to ‘friendly’ access – so that this class, which lives in the same package as the code under test, can see it and replace it with a mock.
Reviewing our method under test – setup()
Let’s look at our setup method:
public void setup(String coffeeDir, String outputDirectory, boolean bare) { String moduleName = projectOperations.getFocusedModuleName(); Element coffeePluginElement = getCoffeeScriptPluginElement(); Document document = coffeePluginElement.getOwnerDocument(); if (bare) { addTextElement(document, coffeePluginElement, "bare", "true"); } else {
addTextElement(document, coffeePluginElement, "bare", COFFEE_DEFAULT_BARE_SETTING); } if (coffeeDir != null && coffeeDir.trim().length() > 0) { addTextElement(document, coffeePluginElement, "coffeeDir", coffeeDir); } else { addTextElement(document, coffeePluginElement, "coffeeDir", COFFEE_DEFAULT_SRC_DIRECTORY); } if (outputDirectory != null && outputDirectory.trim().length() > 0) { addTextElement(document, coffeePluginElement, "coffeeOutputDirectory", outputDirectory); } else { addTextElement(document, coffeePluginElement, "coffeeOutputDirectory", COFFEE_DEFAULT_OUTPUT_DIRECTORY); } projectOperations.addBuildPlugin(moduleName, new Plugin(coffeePluginElement)); }
It’s clear that we have a LOT of branches in this code, but that’s because we’re taking input from our command itself. I’ll lie here, and tell you that I’ve written tests against all of these branches, but again, I said I’m lying – and in a further lie, I’ll tell you that “I’m gonna get to it!” However, here’s why lying doesn’t help – I’m sure I have bugs in this code, and I really need to verify it all.
Oh, and I was thinking – I have a few private methods to help me keep the code organized and modular… Perhaps I should test those too but that leads the way of code smell… Interesting read BTW.
Reviewing the tasks in the method
Ok, the method does a few things:
- Asks a helper method for the Configuration XML file as a basis for the Maven plugin.
- Does a couple of gyrations so that we can maniuplate the plugin nodes with the DOM API – since Roo’s Maven object model is essentially a thin wrapper around the XML API we have to think more in XML. This is something I’ll be exploring in the future
- Sets the options the user passed in.
- Adds the build plugin to the Maven build.
Ultimately, though, we need to see if:
- Given a call to setup(), and the appropriate parameters,
- Does the Plugin contain the proper information
Our test method for the setup process
Ok, here is our sample test method, one that tests the setup of the Coffeescript add-on:
@Test public void testSetupCoffeescript() { when(coffeescriptOperations.projectOperations .getFocusedProjectName()).thenReturn("foo"); // a way for Mockito to grab passed input parameters for testing ArgumentCaptorpluginCaptor = ArgumentCaptor.forClass(Plugin.class); // invoke our method coffeescriptOperations.setup("baz", "bar", false); // did we call addBuildPlugin? Also, notice we capture what the // method passed to the mocked projectOperations.addBuildPlugin method // for the plugin XML Element code verify(coffeescriptOperations.projectOperations) .addBuildPlugin(any(String.class), pluginCaptor.capture()); // Since the plugin has been called and we've captured the method's // second argument, we'll pluck it out and take a gander... Plugin coffeescriptPlugin = pluginCaptor.getValue(); // make sure they passed something! assertNotNull(coffeescriptPlugin); // checks against the model Assert.assertEquals("false", coffeescriptPlugin.getConfiguration() .getConfiguration().getElementsByTagName("bare") .item(0).getTextContent()); Assert.assertEquals("bar", coffeescriptPlugin.getConfiguration() .getConfiguration().getElementsByTagName("coffeeOutputDirectory") .item(0).getTextContent()); Assert.assertEquals("baz", coffeescriptPlugin.getConfiguration() .getConfiguration().getElementsByTagName("coffeeDir") .item(0).getTextContent());
}
Mockito’s ArgumentCaptor
I guess this is really a testing tools article, rather than a Roo article.
The ArgumentCaptor
API is really useful to see what the values were for a mock that was called by your class under test. This is a way to verify that we were passing in the right plugin configuration to our Roo projectManager
, which, after all, we aren’t testing. That’s the Roo team’s job!
Wrap-up
Looking at it from a distance, Roo is just a Java platform that generates, manipulates and configures applications. So it can really do anything. However, rather than testing by re-deploying 10 times, we can run a fast Junit test 10 times instead.
Silly Weasel – Roo add-ons Forge
I launched the Silly Weasel Roo add-ons project, which I’ve open sourced, as a way to promote writing add-ons. I’ll be posting articles about how to write add-ons on that site. On the project homepage you’ll see the OBR URL for getting my Coffeescript, jQuery and (soon) Site add-ons. You can browse my maven repository (the same URL without the repository.xml ending) and grab the source for anything I’ve released.