Setting aside the discussion of stubs Vs. mocks in JUnit testing (that is covered more than adequately elsewhere), let’s say you’ve decided to use stubs, for whatever reason.
Please understand that the simple examples in this article may be more appropriately handled with mocks; they are for illustrative purposes only
Now, let’s say your class under test (CUT) uses a data access object (DAO) that has many methods, but it only needs one or two of those methods. The problem with a stubbed interface is you have to provide implementations of all the methods on the interface. Sure, modern IDEs such as eclipse can do the grunt work and generate all the method shells for you, with you being left to implement just the method(s) you need. However, especially with large stubbed interfaces, you can end up with a large amount of boilerplate code that can easily add up to 90%+ of the LoC in your test case.
This article discusses how to use dynamic proxies to eliminate this problem.
The technique allows you to create a class that implements just those methods you expect your CUT to use.
The examples below use JDK 1.4 syntax; at the end of the article, I provide the full project files in both JDK 5 and 1.4 versions, where the JDK 5 uses the strong typing provided by generics.
Onward…
First, declare the stub wrapper that creates the proxy; I prefer to make this a nested class within the test case so that I can invoke JUnit methods to make tests fail gracefully.
public class MyTest extends TestCase {
//...
private static class StubWrapper implements InvocationHandler {
private Object wrappedStub;
private Class clazz;
public StubWrapper(Object wrappedStub) {
super();
this.wrappedStub = wrappedStub;
this.clazz = wrappedStub.getClass();
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
try {
return clazz.getMethod(method.getName(),
method.getParameterTypes()).invoke(wrappedStub, args);
} catch (Throwable t) {
t.printStackTrace();
fail("Stub method invocation failed");
return null;
}
}
public static Object wrap(Class type, Object stub) {
return Proxy.newProxyInstance(StubWrapper.class
.getClassLoader(), new Class[] { type }, new StubWrapper(
stub));
}
}
}
The static method at line 25 creates a new instance of our proxy; the caller passes the interface in the parameter type and an instance of the class that implements the desired method(s) in parameter stub; the method uses the JDK Proxy mechanism to create the proxied stub. Note that we pass in a new instance of the StubWrapper, which has a reference to user supplied implementation instance.
The invoke method at line 13 is called when the CUT calls a method on the interface. This method is called by the proxy, passing in the proxy instance, the method that was invoked and a list of the arguments that were passed in. It simply delegates (via reflection) the method call to the wrapped stub, with an identical method name and parameter type list.
Note: if the method has not been implemented, we invoke the JUnit fail() method with an appropriate diagnostic.This is an added benefit of the technique. WIth the classic stub technique, if another developer adds a call to another method (in the CUT) there is nothing to force him or her to fill out the stub and the test may continue to pass even though no such implementation is provided. With this technique, the JUnit is sure to fail in such a circumstance.
Now we implement the stub itself…
public class MyTest extends TestCase {
//...
private boolean daoCalled;
//...
// no need to implement the interface; we get here via a proxy
private class StubDAO{
public Collection findSomeStuff() {
daoCalled = true;
MyVO vo = new MyVO();
vo.setSomeAttribute("someValue");
ArrayList l = new ArrayList();
l.add(vo);
return l;
}
}
//...
}
This is a simple test case that returns a list of value objects. Note that we set a boolean so we can assert later that the stub was called.
Instantiate the stub…
public class MyTest extends TestCase {
private MyDAO stubDAO = (MyDAO) StubWrapper.wrap(MyDAO.class, new StubDAO());
//...
}
See how we simply invoke the StubWrapper.wrap method to obtain our proxy that implements the MyDAO interface.
The cast to (MyDAO) is not needed in the JDK 5 version because the wrap method is strongly typed to return an instance of the class passed in as the type parameter.
Finally, we implement the test…
public class MyTest extends TestCase {
private MyDAO stubDAO = (MyDAO) StubWrapper.wrap(MyDAO.class, new StubDAO());
private boolean daoCalled;
public void testMyClassUnderTest() {
CUT cut = new CUT();
cut.setDao(stubDAO);
cut.doSomething();
assertTrue("Expected DAO call", daoCalled);
}
//..
}
We instantiate the CUT, inject the stub, call some method on the CUT (which calls the stub) and assert that the stub was called.
Putting it all together…
package net.gprussell.test;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collection;
import junit.framework.TestCase;
import net.gprussell.CUT;
import net.gprussell.MyDAO;
import net.gprussell.MyVO;
public class MyTest extends TestCase {
private MyDAO stubDAO = (MyDAO) StubWrapper.wrap(MyDAO.class, new StubDAO());
private boolean daoCalled;
public void testMyClassUnderTest() {
CUT cut = new CUT();
cut.setDao(stubDAO);
cut.doSomething();
assertTrue("Expected DAO call", daoCalled);
}
// no need to implement the interface; we get here via a proxy
private class StubDAO{
public Collection findSomeStuff() {
daoCalled = true;
MyVO vo = new MyVO();
vo.setSomeAttribute("someValue");
ArrayList l = new ArrayList();
l.add(vo);
return l;
}
}
private static class StubWrapper implements InvocationHandler {
private Object wrappedStub;
private Class clazz;
public StubWrapper(Object wrappedStub) {
super();
this.wrappedStub = wrappedStub;
this.clazz = wrappedStub.getClass();
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
try {
return clazz.getMethod(method.getName(),
method.getParameterTypes()).invoke(wrappedStub, args);
} catch (Throwable t) {
t.printStackTrace();
fail("Stub method invocation failed");
return null;
}
}
public static Object wrap(Class type, Object stub) {
return Proxy.newProxyInstance(StubWrapper.class
.getClassLoader(), new Class[] { type }, new StubWrapper(
stub));
}
}
}
This zip file contains the JDK 5 version of the above code (eclipse project).
This zip file contains the JDK1.4 version of the code (eclipse project).
Gary
about
