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.
Notes:
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.
Notes:
See the full source code in the JSF EL Validator's NotifyingCompilationManagerTest.groovy.
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.
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.
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.