Groovy: Creating an Interface Stub and Intercepting All Calls to It

It's sometimes useful for unit testing to be able to create a simple no-op stub of an interface the class under test depends upon and to intercept all calls to the stub, for example to remember all the calls and parameters so that you can later verify that they've been invoked as expected. Often you'd use something like Mockito and its verify method but if you're writing unit tests in Groovy as I recommend then there is a simpler and in a way a more powerful alternative.

Solution 1 - When Calling the Stub From a Groovy Object


@Before
public void setUp() {
   this.collectedCalls = []
   // The following works when a method on the listener is called from Groovy but not from Java:
   this.listener = {} as PageNodeListener
   listener.metaClass.invokeMethod = { name, args ->
      collectedCalls << new Call(method: name, args: args) // Call is a simple structure class
   }
}

@Test public void listener_stub_should_report_all_calls_to_it() throws Exception { listener.fileEntered("fileName.dummy") assert collectedCalls.find { it.equals(new Call(method: "fileEntered", args: ["fileName.dummy"]))} }


Notes:
  • 5: {} as PageNodeListener uses Groovy's closure coercion - basically it creates an implementation of the interface which uses the (empty) closure whenever any method is called (we could capture its arguments but not the method name via {Object[] args -> /*...*/} as PageNodeListener)
  • 6-7: We then specify an interceptor method that should be invoked whenever any method is called on the instance
  • Groovy makes it very easy to create beans like Call with equals, toString etc.
Beware that using a coerced closure as an interface implementation works only for methods that are either void or can return null (which is the return value of an empty closure, when invoked). Other methods will throw an NPE:


def list = {} as java.util.List
list.clear()    // OK, void
list.get(0)     // OK, returns null
list.isEmpty()  // NullPointerException at $Proxy4.isEmpty


Alternatively, you may use Groovy's built-in mocking with MockFor and StubFor (from the groovy.mock.interceptor package). The caller of the mock/stub must be in Groovy.

Solution 2 - When Calling the Stub From a Java Object

The solution 1 is small and elegant but for me it hasn't worked when the stub was invoked by a Java object (a clarification of why that happened would be welcome). Fortunately Christoph Metzendorf proposed a solution at StackOverflow (adjusted):


@Before
public void setUp() {
   this.collectedCalls = []
   PageNodeListener listener = createListenerLoggingCallsInto(collectedCalls)
   this.parser = new MyFaces21ValidatingFaceletsParser(TEST_WEBROOT, listener)
}

def createListenerLoggingCallsInto(collectedCalls) { def map = [:]

PageNodeListener.class.methods.each() { method -> map."$method.name" = { Object[] args -> collectedCalls << new Call(method.name, args) } }

return map.asType(PageNodeListener.class) }

@Test public void should_notify_about_file_entered() throws Exception { parser.validateExpressionsInView(toUrl("empty.xhtml"), "/empty.xhtml") assert collectedCalls.find { it.equals(new Call(method: "fileEntered", args: ["/empty.xhtml"]))} }


Notes:
  • 12-13: We create a map containing {method name} -> {closure} for each of the interface's method
  • 17: The map is then coerced to the interface (the same as someMap as PageNodeListener). Notice that if it didn't contain an entry for a method then it would throw a NullPointerException if the method was invoked on the stub.
Notice that this version is more flexible than the Groovy-only one because we have full access to java.lang.reflect.Method when we create the map and thus can adjust the return value of the method closure w.r.t. what is expected. Thus it's possible to stub any interface, even if it has methods returning primitive types.

Conclusion

This is a nice and simple way to stub an interface with methods that are void or return non-primitive values and collect all calls to the stub for a verification. If your requirements differ then you might be better of with a different type of mocking in Groovy or with a true mocking library.

Additional Information

Groovy version: 1.8.2, Java: 6.0.

See the full source code in the JSF EL Validator's NotifyingCompilationManagerTest.groovy.

Tags: testing java groovy


Copyright © 2024 Jakub Holý
Powered by Cryogen
Theme by KingMob