Monday, September 24, 2007

EasyMock testing: isolating methods with partial mocks

Today I was faced with testing a method that presented two difficulties. First, the method under test was overriding a superclass method and invoking the superclass' same-named method. Second, the method needed to invoke an unavoidable static method on a utility class. I managed to work around both difficulties with essentially the same approach, instead of invoking the difficult to mock methods directly (super.whatever() and Utility.staticMethod()), delegate to a helper method, which can then be mocked with a partial mock.

Here are the details.

The method under test overrides getAction(ActionContext context, String type, ActionConfig actionConfig).

    @Override
protected synchronized Action getAction(ActionContext context, String type, ActionConfig actionConfig) throws Exception { // NOPMD
Action action = super.getAction(context, type, actionConfig);
Class actionClass = action.getClass();
Method[] methods = actionClass.getMethods();

/*
* Determine if methods exist that need to be injected with
* beans from the ApplicationContext.
*/
for (Method method : methods) {
if (method.isAnnotationPresent(InjectBean.class)) {
InjectingCreateAction.LOG.info("Found annotation on: " + actionClass.getName());
Class< ? > clazz = method.getParameterTypes()[0];
Object bean = ApplicationContextUtils.getBean(clazz);
method.invoke(action, new Object[] { bean }); // NOPMD
}
}
return action;
}

The two methods presenting some difficulties are highlighted in yellow. A straightforward EasyMock test of this method would fail on these two methods, unless they were run in an environment where these methods could successfully execute. Since the class extends Struts' CreateAction to inject dependencies on Action classes, calling super.getAction() would require all the necessary Struts pieces to be in place when this unit test runs. That's more than we need to test this functionality.

Using EasyMock's class extension 2.2.2+, we can create a partial mock of the class under test, and use this with some simple refactoring to isolate this method to make it testable.

    @Override
protected synchronized Action getAction(ActionContext context, String type, ActionConfig actionConfig) throws Exception { // NOPMD
Action action = getActionObject(context, type, actionConfig);
Class actionClass = action.getClass();
Method[] methods = actionClass.getMethods();

/*
* Determine if methods exist that need to be injected with
* beans from the ApplicationContext.
*/
for (Method method : methods) {
if (method.isAnnotationPresent(InjectBean.class)) {
InjectingCreateAction.LOG.info("Found annotation on: " + actionClass.getName());
Class< ? > clazz = method.getParameterTypes()[0];
Object bean = getBean(clazz);
method.invoke(action, new Object[] { bean }); // NOPMD
}
}

return action;
}

/**
* Delegate method to allow for mock testing of getAction method, which would
* otherwise call a static method on {@link ApplicationContextUtils}.
* @param <T> the class of the return object (inferred)
* @param clazz the class of the requested object (explicit)
* @return an object from the ApplicationContext matching clazz
*/
public <T> T getBean(Class<T> clazz) {
return ApplicationContextUtils.getBean(clazz);
}

/**
* Delegate method to allow for mock testing of getAction method which overrides
* {@link CreateAction#getAction(ActionContext, String, ActionConfig)}.
* @param context ActionContext
* @param type String
* @param actionConfig ActionConfig
* @return The configured Action object
* @throws Exception pass-through
*/
public Action getActionObject(ActionContext context, String type, ActionConfig actionConfig) throws Exception { // NOPMD
return super.getAction(context, type, actionConfig);
}

We have created two delegate methods to do the dirty work of invoking the static method and calling the super class method. Here's how we put that to use with a partial mock.

    public void testGetAction() throws Exception {
InjectingCreateAction partialMock =
EasyMock.createMock(InjectingCreateAction.class,
new Method[] {
InjectingCreateAction.class.getMethod("getActionObject",
ActionContext.class,
String.class,
ActionConfig.class),
InjectingCreateAction.class.getMethod("getBean",
Class.class),

});

IWorkflowController bean = EasyMock.createMock(IWorkflowController.class);
SmokeTestAction test = new SmokeTestAction();
EasyMock.expect(partialMock.getActionObject(
(ActionContext) EasyMock.anyObject(),
(String) EasyMock.anyObject(),
(ActionConfig) EasyMock.anyObject())).andReturn(test);
EasyMock.expect(partialMock.getBean(IWorkflowController.class))
.andReturn(bean);

test.setWfService(bean);

EasyMock.replay(partialMock);
EasyMock.replay(bean);

Action action = partialMock.getAction(null, null, null);
assertNotNull(action);
EasyMock.verify(partialMock);
}

Creating the EasyMock with an array of Methods tells EasyMock to only mock these methods, not the entire class. The remaining calls are passed through to the underlying class. The remainder of the test is standard EasyMock stuff.

2 comments:

ktsnj said...
This comment has been removed by a blog administrator.
Szczepan Faber said...

I was more thinking about such a hack: injecting this to... this :)

Action action = delegate.getActionObject(context, type, actionConfig);
...
Object bean = delegate.getBean(clazz);

Then partial mocking is no longer necessary...