Thoughts on using Groovy for unit tests

I’ve been working with Groovy in my current contract.  I’d been hearing about it for a few years, ever since some JavaOne fireside chat mentioned it as an example of the future flexibility of the JVM, but I hadn’t done anything with it.  Over the past few months I’ve come to appreciate it, especially for use in writing unit tests.

I believe that even with my less-than-complete knowledge of Groovy, I’m able to produce useful tests more quickly in it than by writing the equivalent Java code.  Granted, the tests may seem a little “sloppy” compared with equivalent Java code, but in tests, the trade-off has often been worth it.  Further, one nice aspect to Groovy is that if you need the more disciplined Java structure, you can just write Java!  So here are some Groovy features I’ve found useful when writing tests.

Useful additional test assertions, such as shouldFail() and shouldFailWithCause()

I am able to replace:

try {
  test.doUnsupportedOperation();
  fail("Should have thrown exception");
} catch (UnsupportedOperationException e) {
  // expected
}

With:

shouldFail(UnsupportedOperationException) {
  test.doUnsupportedOperation()
}

And get a generally equivalent result with better readability, less typing, and without having to write my own method.  I found some minor glitches when using these methods with mocks, but they are usually handled by using shouldFailWithCause()  or some other simple workaround in the test.

Easy creation of mock Interface implementations using Map coercion.

(My current workplace uses EasyMock for a mock framework so that’s what I talk about, but most of this would be true about other frameworks as well, I bet.)

EasyMock is a very powerful tool when you need it to be.  But if you’re just looking for quick mocks to inject into the class being tested, there can be a lot of extra typing.  I can replace:

ExamRegistration exam = EasyMock.createMock(ExamRegistration.class);
EasyMock.expect(exam.getID()).andReturn(200).anyTimes();
EasyMock.replay(exam);

With:

def exam = [getID: {200}] as ExamRegistration

And get the same behavior in the test.  If a method is called that isn’t mocked, a NullPointerException  is thrown.  The closures used as values in the map can refere to other variables in the test, which allows you to do counts, share values, etc.  For example, if it is important to actually verify that exam was called, a flag could be set within the closure and checked during test validation. This approach separates the test setup, execution, and validation stages nicely, where using EasyMock blurs those lines.

EasyMock can still be used, and there are several situations where it is advantageous:

  • If you need to mock a Java class, use the EasyMock extension packages.  Coercion works with Groovy classes (and EasyMock doesn’t).
  • If you are testing exception handling, particularly types of exceptions thrown.  While throwing exceptions from a mock works just fine, catching them within the code being tested doesn’t always work.  I haven’t figured out why.
  • If you want verification of more complex mock behavior.  This could change if someone writes some flexible helper methods in Groovy, but for now…

Bottom line: There is a place for EasyMock, but overall, my use of it has gone down substantially since starting to write Groovy unit tests.

Native language support for collections

Lists and Maps are extremely easy to work with, and converting to Sets or arrays is trivial.  This can really streamline some code, especially given Groovy’s dynamic typing.  For example, if you’re testing a method: List<String> foo(Map<String, List<String>>)

You could test sending empty parameters in Java:

List<String> results = test.foo(new HashMap<String, List<String>>());

And in Groovy:

def results = test.foo([:])

This gets more noticeable when you\’re testing with non-empty collections:

Map<String, List<String>> params = new HashMap<String, List<String>>();
params.put("key1", Arrays.asList("val1", "val2"));
params.put("key2", Arrays.asList("val3", "val4", "val5"));

List<String> results = test.foo(params);

And in Groovy:

def results = test.foo([
    'key1': ['val1', 'val2'],
    'key2': ['val3', 'val4', 'val5']
])

(Yes, I know both examples could be compressed even more, but I’m trying to do a good-faith balance between typical usage and clarity…)

In addition, there are many built in methods to create, iterate, and modify collections which I’m still learning how to leverage more fully. In general, I’ve appreciated the more compact, lower signal-to-noise coding that Groovy offers in dealing with collections.

Dynamic types and casting

This isn’t something I’m using as much as I probably could, because I’ve still got Java reflexes, but I know that the newer test classes I’m writing leverage this a lot more.  While it’s definitely a two-edged sword, especially if your IDE has inspection rules that check for types (IDEA can do this, for example), it can speed up some coding.  I’ve been using it more as a part of some of the previously mentioned features, within mocks and especially manipulating collections.

Seamless Java integration

I didn’t quite know how to put this, but I just mean that Java code is legal Groovy code, and that\’s a stress-reducer.  If I get stuck someplace, I know I can just write the Java code within a Groovy class.  Again, there are a couple of things to keep in mind, and the more Groovy classes are being used within tests, the less useful this probably becomes, but it has been a comforting thought to keep in the back of my mind as I move forward with Groovy tests.

There are other useful language features (a more full use of closures would be the biggest one that comes to mind) that I can see some real potential use for, but I don’t have the knowledge to really use effectively yet.  I would just say that, all things being equal, if the environment allows it, I would generally prefer to write unit tests in Groovy.

I’ll try to write up some gotchas or speedbumps I’ve hit using Groovy soon…

Leave a Reply

Your email address will not be published. Required fields are marked *