Mar 24th 2008 05:22 am Unit Testing Isolated Methods with Groovy

Glen Smith has been running a great series of posts this month to help people get started with unit testing. In that sprit, I thought I’d put out a post on some unit testing work that I’ve been doing recently.

There are a number of libraries and techniques for unit testing with groovy. It comes with MockFor and StubFor out of the box, and you can also leverage Java libraries like EasyMock and Mockito if your needs aren’t satisfied by the built-in constructs.

The mock and stub implementations that I mention above work well for allowing you to control the behavior of any collaborators, but what if your method calls another method within the same class? None of those solutions (as far as I’m aware) allow you to replace the behavior of other methods on the Class Under Test (CUT). If you are trying to unit test your method in true isolation, you need the ability to stub out the behavior of internal methods on the class (for example, to throw an exception to test try/catch logic).

If you had the following CUT:

class ClassUnderTest {
    def testMe() {
        return "${first()}, ${second()}"
    }
    def first() {
        return "first"
    }
    def second() {
        return "second"
    }
}

If you’re trying to unit test the testMe method, you might want to control the values that come back from first and second to ensure that testMe still reacts as you expect.

This test helper class will allow you to test your methods in isolation, so you can focus on testing the logic in your Method Under Test (MUT), rather than worrying about the contortions that might be necessary to get helper methods into the state you want to test.

com.naleid.test.TestHelper.groovy:

package com.naleid.test
 
class TestHelper {
    public static def isolateMethod = { methodRef, stubMethods, closure ->
        def clazz = methodRef.owner.theClass
        clazz.metaClass.invokeMethod = { String name, args ->
            def stubMethod = stubMethods[name]
            if (methodRef.method == name || stubMethod?.owner == clazz) {
                // invoke a real method on the class , it's either the method we're testing in isolation or
                // one we've been told to pass through
                return clazz.metaClass.getMetaMethod(name, args).invoke(delegate, args)
            } else if (stubMethod) { // we have a stub method to use
                return stubMethods[name].call(args)
            } else {
                throw new Exception("Testing method in isolation; other method called that we haven't been provided a stub for: $name($args)")
            }
        }
 
        closure.call() // call the code that tests methodRef in isolation
 
        GroovySystem.metaClassRegistry.removeMetaClass(clazz) // clean up after ourselves
    }
}

What’s going on here? We’ve got a static method isolateMethod that allows us to pass in a method reference, a map of stub methods that we can control the behavior of, and a closure that will actually execute the tests while we have our method in isolation. We temporarily modify the metaClass of our CUT so that we intercept all calls to it’s instances. Then we execute the test closure that we were passed. Finally, we clean up after ourselves by reverting our CUT’s metaClass back to it’s original state.

There is some similarity here to the use keyword employed by MockFor and StubFor.

Well, it’s probably easier to show rather than just tell. Here is a GroovyTestCase that uses the static isolateMethod to change the behavior of first and second.

import com.naleid.test.TestHelper as TH
 
class IsolateMethodTests extends GroovyTestCase {
 
    def stubFirst = { return "stubFirst" }
    def stubSecond = { return "stubSecond" }
 
    void testIsolateMethod() {
        assertEquals("first, second", new ClassUnderTest().testMe()) // original behavior
 
        TH.isolateMethod(ClassUnderTest.metaClass.&testMe, [first: stubFirst, second: stubSecond]) {
            assertEquals("stubFirst, stubSecond",  new ClassUnderTest().testMe()) // stubbed out method behavior
        }
 
        assertEquals("first, second", new ClassUnderTest().testMe()) // back to original behavior
    }
}

We call the TestHelper’s isolateMethod and pass in the following items:

  • ClassUnderTest.metaClass.&testMe - a reference to the testMe method that we get from ClassUnderTest’s metaClass
  • [first: stubFirst, second: stubSecond] - a map of method names to stub closures that have replacement behavior for us to use

We also give it a closure that shows that we’re using stub methods rather than the real methods.

What if you still want the behavior of some of the sub-methods that testMe calls? All you have to do is pass in a reference to the real method and isolateMethod will pass it through.

void testIsolateMethodStubFirstRealSecond() {
    TH.isolateMethod(ClassUnderTest.metaClass.&testMe, [first: stubFirst, second: ClassUnderTest.&second]) {
        assertEquals("stubFirst, second", new ClassUnderTest().testMe())
    }
}

isolateMethod is also designed to throw an exception if a method that you haven’t given a stub is called by the MUT. Here is an example that shows what happens where we’ve given first, but neglected to include a stub for second:

void testIsolateMethodThrowsExceptionForMissingMethod() {
    TH.isolateMethod(ClassUnderTest.metaClass.&testMe, [first: stubFirst]) {
 
    def e = shouldFailWithCause(Exception) {
        new ClassUnderTest().testMe()
    }
    assertEquals("Testing method in isolation; other method called that we haven't been provided a stub for: second({})", e.toString())
    }
}

Using a technique like isolateMethod has the effect of making mocking libraries and unit testing much easier to work with, as you don’t need to worry about mocking out all of the collaborators that sub-methods of the MUT interact with.

I’m curious to hear how others have dealt with this issue in their unit testing in the past. Are there other solutions out there that I’ve missed?

Posted by tednaleid / grails and groovy and unit testing

2 Responses to “Unit Testing Isolated Methods with Groovy”

  1. Hamlet D'Arcy on 30 Mar 2008 at 11:49 am #

    Very clever.There are a couple other solutions I use that work in Java as well.

    1. Write a test specific subclass and override the method you want to mock out. http://xunitpatterns.com/Test-Specific%20Subclass.html

    2. Use the EasyMock class extensions and create a partial mock. http://easymock.org/EasyMock2_3_ClassExtension_Documentation.html

    The calling semantics of your test helper is pretty nice though!

  2. tednaleid on 01 Apr 2008 at 3:18 am #

    Hamlet, thanks for your response!

    I’ve only ever used the interface mocking abilities of EasyMock and haven’t tried it with the class extensions. From a brief look, it appears that using the class extensions to create a partial mock is pretty similar to what I’m doing above. It’s pulling a reference to the method under test and mocking out the rest of the class.

    The Test-Specific Subclass pattern is another good alternative. Though it sounds like it might be a bit of work to create a new subclass and maintain the additional instrumentation on the subclass that exposes and controls the state for testing.

    I could definitely see that pattern being useful when working with legacy code that you don’t have much control over.

Trackback URI | Comments RSS

Leave a Reply