Testing EJBs without a container

One of the more annoying aspects of testing EJBs has always been the fact you need to bundle them up in a JAR (and often an EAR) and deploy them to a server to thoroughly test them. This process drags out the development of unit tests, and makes life generally painful.

As of EJB 3, however, it’s no longer necessary. instead, it is fairly trivial to mock out the container entirely (and safely!)


I’m not talking about classic unit-tests, where _everything_ is mocked out. You could already do that with EJB2 without too much hassle. No, I’m talking about tests that basically wire everything up the way it is when deployed, including such things as:

* references to other EJBs;
* database connections;
* security; and
* transactional support

How do you do it? Simple: you write a factory that creates your EJB instance and wires it up for you. This can be done through the power of reflection – by looking at the annotations.

(In theory, you could have done this with EJB2, as well, but the external deployment descriptor made it painful).

Enough talk. Here’s some code that demonstrates this:

public class EjbFactory {
  
  public interface EjbMocker {
     void provideMocks(T bean);
  }
  
  private Map beans = new HashMap();
  private final EjbMocker mocker;
  
  public EjbFactory() {
    this(new NoMocks());
  }
  public EjbFactory(EjbMocker mocker) {
    this.mocker = mocker;
  }
  
  public  T createEjb(Class service) {
    T bean = createBean(service);
    T decoratedBean = decorate(bean);
    registerEjb(service, decoratedBean);
    addEjbRefs(bean);
    runCustomInterceptors(bean);
    postConstruct(bean);
    mocker.provideMocks(bean);
    return decoratedBean;
  }
  
  private  void runCustomInterceptors(T bean) {
    if (bean.getClass().isAnnotationPresent(Interceptors.class)) {
      // TODO: This really should be built up as a chain, not a loop.
      Class[] interceptors = bean.getClass().getAnnotation(Interceptors.class).value();
      InvocationContext context = new MockInvocationContext(bean);
      for (Class interceptorClazz : interceptors) {
        postConstruct(newInstance(interceptorClazz), context);
      }
    }
  }
  private  T newInstance(Class clazz) {
    try {
      return clazz.newInstance();
    } catch (InstantiationException e) {
      throw new RuntimeException(e);
    } catch (IllegalAccessException e) {
      throw new RuntimeException(e);
    }
  }
  
  private  T createBean(Class service) {
    try {
      if (service.isAnnotationPresent(Stateless.class)) {
        return newInstance(service);
      }
      if (service.isInterface() == false) {
        throw new RuntimeException("Trying to mock a non-service: " + service.getName());
      }
      Class serviceClass = Class.forName(service.getName() + "Bean");
      return (T) newInstance(serviceClass);
    } catch (ClassNotFoundException e) {
      throw new RuntimeException("Could not find bean class for " + service.getName());
    }
  }
  
  private  Object registerEjb(Class service, T bean) {
    return beans.put(service, bean);
  }
  
  private  T decorate(T bean) {
    return bean;
  }
  
  private  void addEjbRefs(T bean) {
    Class beanClazz = bean.getClass();
    for (Method method : beanClazz.getMethods()) {
      if (method.isAnnotationPresent(EJB.class) && ejbIsIgnored(method) == false) {
        setEjbRef(bean, method);
      }
    }
  }
  
  private boolean ejbIsIgnored(Method method) {
    List ignoredBeans = getIgnoredBeans();
    return ignoredBeans.contains(determineBeanClass(method));  
  }
  
  private List getIgnoredBeans() {
    if (mocker.getClass().isAnnotationPresent(IgnoredEjbs.class) == false) {
      return Collections.emptyList();
    }
    Class[] ignoredEjbs = mocker.getClass().getAnnotation(IgnoredEjbs.class).value();
    return Arrays.asList(ignoredEjbs);
  }
  
  private  void setEjbRef(T bean, Method setter) {
    Class otherBeanClass = determineBeanClass(setter);
    Object otherBean = lookupBean(otherBeanClass);
    invoke(bean, setter, otherBean);
  }
  
  private Class determineBeanClass(Method setter) {
    EJB ejbRefAnnotation = setter.getAnnotation(EJB.class);
    if (ejbRefAnnotation.beanInterface() != null &&
        Object.class.equals(ejbRefAnnotation.beanInterface()) == false) {
      return ejbRefAnnotation.beanInterface();
    }
    Class otherBeanClass = setter.getParameterTypes()[0];
    return otherBeanClass;
  }
  
  private  void invoke(T bean, Method method, Object ... args) {
    try {
      method.invoke(bean, args);
    } catch (IllegalArgumentException e) {
      throw new RuntimeException(e);
    } catch (IllegalAccessException e) {
      throw new RuntimeException(e);
    } catch (InvocationTargetException e) {
      throw new RuntimeException(e.getCause());
    }
  }
  
  private Object lookupBean(Class otherBeanClass) {
    if (beans.containsKey(otherBeanClass)) {
      return beans.get(otherBeanClass);
    }
    return createEjb(otherBeanClass);
  }
  
  private  void postConstruct(T bean) {
    Class beanClazz = bean.getClass();
    for (Method method : beanClazz.getMethods()) {
      if (method.isAnnotationPresent(PostConstruct.class)) {
        invoke(bean, method);
      }
    }
  }
  
  private void postConstruct(Object bean, InvocationContext context) {
    Class beanClazz = bean.getClass();
    for (Method method : beanClazz.getMethods()) {
      if (method.isAnnotationPresent(PostConstruct.class)) {
        invoke(bean, method, context);
      }
    }
  }
  
  private static class NoMocks implements EjbMocker {
    public  void provideMocks(T bean) {
      // do squat.
    }
  }
  
  private static class MockInvocationContext implements InvocationContext {
    private final Object bean;
    
    public MockInvocationContext(Object bean) { this.bean = bean; }
    
    public Object getTarget() { return bean; }
    
    public Object proceed() throws Exception {
      return null;
    }
    // ... more mock methods here.
  }
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoredEjbs {
Class[] value() default { };
}

Let me just run you through this…

You create a stubbed-out EJB like this: new EjbFactory.create(MyService.class). The createBean() method does the following things:

* create an instance of your EJB (using a simple naming convention, in this case, to find the implementing class – MyServiceBean)
* decorate the bean. In this early version, it does nothing. However, this would allow you to use dynamic proxies to implement security checking and transactional support.
* add references to other EJBs, by looking for @EJB annotations.
* run any interceptors declared for the EJB, and finally
* invoke the post-construct methods.

This example code isn’t complete – it’s based on an early version of what we built at Wotif. For example, there isn’t any support in this for injecting resources (such as database connections – or persistence contexts). Nor is the transactional or security support demonstrated. However, it’s very functional.

A couple of other features worth noticing:

* You can inject mocks or stubs into the created EJBs if you want (for example, we inject a security utility that’s a real pain to use directly).
* You can also ignore EJBs if you want, so that they don’t get stubbed out. Presumably, if you need them, you would inject a mock (I’ve used this to fake out facades to remote servers, for example).

We used this to convert a number of our EJB tests from calling against the deployed server to testing without deploying. As you can imagine, this makes the test-code cycle a lot faster.

While “strong” unit testing is still my preferred option, these more complete tests have their place – especially for testing persistence logic. This makes it a lot easier to test out of container, and reduces (not elimiate!) your need to test against a deployed app.

Advertisements

Author: Robert Watkins

My name is Robert Watkins. I am a software developer and have been for over 18 years now. I currently work for people, but my opinions here are in no way endorsed by them (which is cool; their opinions aren’t endorsed by me either). My main professional interests are in Java development, using Agile methods, with a historical focus on building web based applications. I’m also a Mac-fan and love my iPhone, which I’m currently learning how to code for. I live and work in Brisbane, Australia, but I grew up in the Northern Territory, and still find Brisbane too cold (after 16 years here). I’m married, with two children and one cat. My politics are socialist in tendency, my religious affiliation is atheist (aka “none of the above”), my attitude is condescending and my moral standing is lying down.

2 thoughts on “Testing EJBs without a container”

  1. Have you perhaps considered making the EJBFactory available as an open source project? It sounds like you’ve got a pretty good base built already.

  2. It’s probably a good idea. The one we use is custom for us (ie, amongst other things, we filter the list of interceptors – we don’t want all of them loaded all the time). I’ll see about getting around to it.

    However, everything on this site is free to use (with attribution), including the above code.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s