Saturday 17 May 2008

We've gone Groovy

When we started our latest project about 6 months ago I was banging on about using Groovy. Nobody was convinced or should I say interested.

On Thursday the Mighty Gus and myself went along to a presentation on Groovy. We came back thoroughly enthused and were desperate to use it day to day.

Rather than placing Groovy code directly into the application we thought it would be good to introduce the language in the test suite. This would help familiarise the team with the language before introducing Groovy to the actual web application ( that is if we chose to go ahead and continue using the language ).

All of the developers on the team are competent with Ruby and would keen to use it within a web app. Having a Java web app places a bit of a restriction on introducing Ruby into the app code. This is the major win for Groovy, it seamlessly integrates with the Java code!

The main aim for Thursday afternoon was to
  • get Groovy up and running within our development environment,
  • write a test case for new functionality
  • update the continuous build and try to have minimal impact on the teams local environments.

Getting Groovy up and running locally
Only two downloads were required.
  1. The Groovy compiler
  2. JetGroovy, the IntelliJ plugin
Then we pointed the plugin at the Groovy home and added the groovy-all.jar to the test module path.

At this point we could run groovy test cases against our java codebase!
When running 'All Tests' in IntelliJ it automatically picked up the groovy test case!


Writing a test case for new functionality
Gus and I were half way though some work prior to going to the presentation.
We took the old Java test case and cloned it into a groovy test case ( updated the file and classname to avoid clashes).

At this point we were running groovy tests!

Yet we weren't taking advantage of any of the Groovy-ness, it was simply a renamed Java class.
What Groovy features did we make use of?
  1. Remove semicolons ( as Ben described it, "the bald look" )
  2. Removing method brackets ( this only works in certain situations)
  3. Dropped types where they were not of value.
  4. Introduce groovy lists for those ugly mock objects to return.
  5. Replace the ugly mocks with some super-condensed stubs.
I should explain 4 & 5 above a bit more.
4: Due to the nature of some of our 'generic' interfaces that we were testing, the tests need to know about the 'generic' type. ( Does this sound like a contradiction to anyone else? )

For example:
We were testing a Spring Controller which adds the size of a list of items to the Model for display.
The list of items was retrieved from a Dao, which is injected into the Controller.
Standard practise on our project is to mock the Dao and, in the test, place expectations on the methods that will be called.

public class Controller
{
private Dao dao;

public Controller( Dao dao)
{
this.dao = dao;
}
public void handleRequest( HttpRequest request, HttpResponse response )
{
Map model = new HashMap();
model.put( "number_of_items", dao.getItems().size() );
return new ModelAndView( "viewname", model );
}
}

public class TestControllerJava extends MockObjectTestCase
{
public void testCorrectNumberOfItemsAreAddedToModel()
{
List items = Arrays.asList( new Item(), new Item(), new Item() );
Mock mockDao = mock( Dao.class )
Controller testee = new Controller( (Dao)mockDao.proxy() );

mockDao.expects( once() ).method( "getItems" ).will( returnValue( items ) );

ModelAndView actual = testee.handleRequest( null, null );
assertEquals( items.size(), actual.getModel().get( "number_of_items" );
}
}

Here nothing in the Controller is particularly interested in the type Item. It is only concerned about the number of Items.

Which brings us to introducing Groovy lists.

class TestControllerGroovy extends MockObjectTestCase
{
void testCorrectNumberOfItemsAreAddedToModel()
{
def items = ["one","two","three"]
Mock mockDao = mock( Dao.class )
Controller testee = new Controller( (Dao)mockDao.proxy() );

mockDao.expects( once() ).method( "getItems" ).will( returnValue( items ) );

ModelAndView actual = testee.handleRequest( null, null );
assertEquals( items.size(), actual.getModel().get( "number_of_items" );
}
}


In this example the differnce may not be much, but consider the situation where the construction of the Item object is complex and needs a number of params.

This brings us nicely to further explaination of item 5 above. ( Replace the ugly mocks with some super-condensed stubs.)


class TestControllerGroovy extends TestCase
{

void testCorrectNumberOfItemsAreAddedToModel()

{

def items = ["one","two","three"]

def dao = [getItems: { items }] as Dao

Controller testee = new Controller(
dao );

ModelAndView actual = testee.handleRequest( null, null );

assertEquals( items.size(), actual.getModel().get( "number_of_items" );

}

}


Here we have, line in Javascript, created a map instead of an actual typed object as the stub. We have satisfied just enough of the interface that we are interested in ( or that the source is interested in ).

The map is defined within the square brackets, the first item being the method call, then a colon, then a block ( which is defined in the curly brackets ) which will be executed when the method is called. You can have as many of these as you like, separated by commas. Another point to note is that, just like in Ruby, the result of the last statement executed will be returned by the method. Therefore there is no need to explicitly say {return items} as {items} will do perfectly well.

I'm not suggesting to use this as a rule, always aim for the clearest code/test possible rather than roping in as many new features of a language as possible.

The example above is a great deal simpler that the actual code and test we created on Thursday. It had 7 lists and 7 mock expectations and it looked absolutely hideous. It was so much nicer to see it condensed into 7 lines, however it felt really strange seeing so much functionality defined within 7 lines!

I suspect this is something we will get used to, it just felt weird.

Check in time - or is it?
How will this affect the other developers and the build box?

Our Team City build delegates to Ant. A quick look at the Groovy site gave some nice examples for compiling Groovy in Ant.

What we did forget to do was to add the groovy-all.jar to the ant classpath - D'oh.

We then updated the build box with the appropriate groovy install and jars and put together some info for the other developers about where to find the compiler and JetGroovy.

Job done.

I'll let you know how we get on!
WOOOO