Tapestry: Using jMock and Inject-Object

Problem Statement

We’ve been unit testing Tapestry pages, using the strategy we described over here, and we found ourselves somewhat unsatisfied with one aspect of this testing: the injection of dependent objects into page classes from Spring.

Tapestry allows you to inject an object directly into a page from Spring. One way of accomplishing this is via the InjectObject annotation:

@InjectObject("spring:ca.intelliware.example.service.ReportService")
public abstract ReportService getReportService();

In this case, we depend upon a Spring application context file that includes something like this:

<bean id="ca.intelliware.example.service.ReportService"
  class="ca.intelliware.example.service.impl.ReportServiceImpl">
  ...
</bean>

Now, that works for the real McCoy, but, hey, we’re writing a test. Really, we’d rather have a mock implementation of this service for testing purposes.

Now, we could do this:

<bean id="ca.intelliware.example.service.ReportService"
  class="ca.intelliware.example.service.impl.MockReportServiceImpl">
  ...
</bean>

Set up an alternate (mock) version of the ReportService in a “mock” application context file. But really, this is what cool tools like jMock are for.

Now, it’s hard to use jMock to inject stuff into a Tapestry page because (in the way we’re testing them) we don’t get to control the page life cycle. Hmm. What to do?

Here’s an idea we had. First, Spring allows you to configure beans that implement some special interfaces, such as FactoryBean. The FactoryBean interface serves up instances of the underlying bean.

We created a simple implementation of the FactoryBean that served up instances from a simple HashMap registry.

This allows us to do something like this:

Mock mock = mock(ReportService.class);
mock.expects(once()).method("persist").with(matching(report));
ReportService service = (ReportService) mock.proxy();
JMockBeanFactory.register(ReportService.class, service);

invokeTestOnTapestryPage();

In this example, we are using the JMockBeanFactory to hold mock implementation of our services. The key “glue” code involves our mock Spring application context file:

<bean id="ca.intelliware.example.service.ReportService"
    class="ca.intelliware.util.spring.JMockBeanFactory">
  <property name="keyClass"
    value="ca.intelliware.example.service.BroadcastService" />
</bean>

This configuration ensures that when Tapestry calls Spring to ask for an instance of the bean to inject, it’s going to invoke our JMockBeanFactory.

There was one annoying glitch: Spring very much wants to confirm that all the beans are correctly initialized very early in its setup process. In fact, this often happened before we’d created our mock service. Our solution to that was that we had JMockBeanFactory serve up a simple proxy of our keyClass (which tended to be an interface). This proxy delegated all of its work to the instance in the registry, but it would look up that instance later in the process.

Here’s what our final JMockBeanFactory looked like:

package ca.intelliware.util.spring;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.FactoryBean;

public class JMockBeanFactory implements FactoryBean {

  private class InvocationHandlerImpl implements InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable {
      try {
        return method.invoke(getObjectFromMap(), args);
      } catch (InvocationTargetException e) {
        throw e.getCause();
      }
    }
  }

  private static final Map<Class,Object> MAP =
      Collections.synchronizedMap(new HashMap<Class,Object>());

  private Class keyClass;

  private Object getObjectFromMap() {
    return MAP.get(this.keyClass);
  }

  public Object getObject() throws Exception {
    return Proxy.newProxyInstance(
      this.keyClass.getClassLoader(),
      new Class[] { this.keyClass },
      new InvocationHandlerImpl());
  }

  public Class getObjectType() {
    return this.keyClass;
  }

  public boolean isSingleton() {
    return true;
  }

  public Class getKeyClass() {
    return this.keyClass;
  }

  public void setKeyClass(Class keyClass) {
    this.keyClass = keyClass;
  }

  public static void register(Class keyClass, Object object) {
    MAP.put(keyClass, object);
  }

  public static void clear() {
    MAP.clear();
  }
}

It's only fair to share...
Share on FacebookGoogle+Tweet about this on TwitterShare on LinkedIn

Leave a Reply