Tuesday 27 December 2011

JSF: mocking FacesContext for unit tests

Referencing the FacesContext directly in Java classes makes managed beans more difficult to unit test. This post discusses how to mock the context for testing outside the application container.

These examples use Mockito with JUnit. Familiarity with JSF and unit testing Java is assumed.

I've used the javax.faces.bean annotations but the techniques apply for other bean management mechanisms (e.g. using faces-config.xml or Spring).
  1. Understanding the Lifecycle of the FacesContext
  2. Mocking for FacesContext.getCurrentInstance()
  3. Designing for Test with Dependency Injection
  4. Dependency Injection and Scopes
  5. End Notes

Understanding the Lifecycle of the FacesContext

It is important to note that even though you can reference it via a static method that the FacesContext is not a singleton.

The FacesContext is a request-scope artifact. At the start of a request, the controller (e.g. servlet or portlet) will create a new context using the FacesContextFactory. When it is created, it assigns itself to a ThreadLocal static variable so that it can be referenced via getCurrentInstance(). At the end of the request the controller calls release() to dispose of the context.

Outside of a request, a limited context is made available during application startup and shutdown when only certain documented methods can be called.

A HTTP request in a portlet container may cause the creation and release of a number of FacesContexts as the individual portlets and their scopes are processed.

Mocking for FacesContext.getCurrentInstance()

Here is a managed bean that acquires the FacesContext via a static call:

package foo;

import java.util.Map;

import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
import javax.faces.context.FacesContext;

@ManagedBean
@RequestScoped
public class AlphaBean {
  public String incrementFoo() {
    Map<String, Object> session = FacesContext.getCurrentInstance()
        .getExternalContext()
        .getSessionMap();
    Integer foo = (Integersession.get("foo");
    foo = (foo == null: foo + 1;
    session.put("foo", foo);
    return null;
  }
}

This managed bean increments a session-scoped integer when incrementFoo() is invoked.

To test this I've implemented a utility class that sets the mock via setCurrentInstance(FacesContext):

package foo.test;

import javax.faces.context.FacesContext;

import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

public abstract class ContextMocker extends FacesContext {
  private ContextMocker() {
  }

  private static final Release RELEASE = new Release();

  private static class Release implements Answer<Void> {
    @Override
    public Void answer(InvocationOnMock invocationthrows Throwable {
      setCurrentInstance(null);
      return null;
    }
  }

  public static FacesContext mockFacesContext() {
    FacesContext context = Mockito.mock(FacesContext.class);
    setCurrentInstance(context);
    Mockito.doAnswer(RELEASE)
        .when(context)
        .release();
    return context;
  }
}

For completeness I've added code to release the context for garbage collection at the end of the test. You can omit this code if you don't mind the leak.

The unit test looks like this:

package foo.test;

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.HashMap;
import java.util.Map;

import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;

import org.junit.Test;

import foo.AlphaBean;

public class AlphaBeanTest {
  @Test
  public void testIncrementFoo() {
    FacesContext context = ContextMocker.mockFacesContext();
    try {
      Map<String, Object> session = new HashMap<String, Object>();
      ExternalContext ext = mock(ExternalContext.class);
      when(ext.getSessionMap()).thenReturn(session);
      when(context.getExternalContext()).thenReturn(ext);

      AlphaBean bean = new AlphaBean();
      bean.incrementFoo();
      assertEquals(1, session.get("foo"));
      bean.incrementFoo();
      assertEquals(2, session.get("foo"));
    finally {
      context.release();
    }
  }
}

Designing for Test with Dependency Injection

Static calls aren't the only way to reference the FacesContext. The context is also provisioned via the facesContext request scope variable.

In this version of the bean the context is injected by the framework before the bean is placed into scope:

//headers elided

@ManagedBean
@RequestScoped
public class BetaBean {
  @ManagedProperty("#{facesContext}")
  private FacesContext context;

  public FacesContext getContext() {
    return context;
  }

  public void setContext(FacesContext context) {
    this.context = context;
  }

  public String incrementFoo() {
    Map<String, Object> session = context.getExternalContext()
        .getSessionMap();
    Integer foo = (Integersession.get("foo");
    foo = (foo == null: foo + 1;
    session.put("foo", foo);
    return null;
  }
}

Because the test doesn't instantiate the bean via a resolver the test must set the mock context explicitly:

//headers elided

public class BetaBeanTest {
  @Test
  public void testIncrementFoo() {
    Map<String, Object> session = new HashMap<String, Object>();
    ExternalContext ext = mock(ExternalContext.class);
    when(ext.getSessionMap()).thenReturn(session);
    FacesContext context = mock(FacesContext.class);
    when(context.getExternalContext()).thenReturn(ext);

    BetaBean bean = new BetaBean();
    bean.setContext(context);
    bean.incrementFoo();
    assertEquals(1, session.get("foo"));
    bean.incrementFoo();
    assertEquals(2, session.get("foo"));
  }
}

Dependency Injection and Scopes

JSF's managed properties prevent the injection of narrowly scoped artifacts into broader scopes. This helps prevent stale objects leaking out of scope. It also means that you can't inject #{facesContext} as a managed property into scopes broader than the request scope.

An application scoped utility bean can be used to overcome this problem:

//headers elided

@ManagedBean
@ApplicationScoped
public class FacesBroker implements Serializable {
  private static final long serialVersionUID = 1L;

  private static final FacesBroker INSTANCE = new FacesBroker();

  public FacesContext getContext() {
    return FacesContext.getCurrentInstance();
  }

  private Object readResolve() throws ObjectStreamException {
    return INSTANCE;
  }
}

The class is serializable to allow it to work in sessions that are passivated out of memory. See the servlet specification for more details.

Managed beans must always reference the context via this broker:

//headers elided

@ManagedBean
@ApplicationScoped
public class GammaBean {
  @ManagedProperty("#{facesBroker}")
  private FacesBroker broker;

  public FacesBroker getBroker() {
    return broker;
  }

  public void setBroker(FacesBroker broker) {
    this.broker = broker;
  }

  public String incrementFoo() {
    Map<String, Object> session = broker.getContext()
        .getExternalContext()
        .getSessionMap();
    Integer foo = (Integersession.get("foo");
    foo = (foo == null: foo + 1;
    session.put("foo", foo);
    return null;
  }
}

The corresponding unit test:

//headers elided

public class GammaBeanTest {
  @Test
  public void testIncrementFoo() {
    Map<String, Object> session = new HashMap<String, Object>();
    ExternalContext ext = mock(ExternalContext.class);
    when(ext.getSessionMap()).thenReturn(session);
    FacesContext context = mock(FacesContext.class);
    when(context.getExternalContext()).thenReturn(ext);
    FacesBroker broker = mock(FacesBroker.class);
    when(broker.getContext()).thenReturn(context);

    GammaBean bean = new GammaBean();
    bean.setBroker(broker);
    bean.incrementFoo();
    assertEquals(1, session.get("foo"));
    bean.incrementFoo();
    assertEquals(2, session.get("foo"));
  }
}

End Notes

Versions used:

  • Java 6
  • JSF 2
  • JUnit 4
  • Mockito 1.8

5 comments:

  1. Thank you for your post. I am trying to do myself a ContextMocker but I ma not able to use "import javax.faces.context.FacesContext;"
    i got "java ee api is missing on project classpath" even I have in my maven pom.xml

    javax
    javaee-api
    6.0

    Can you give me a hint?
    Thank you.

    ReplyDelete
  2. "java ee api is missing on project classpath" does not sound like a Maven error; if you are having problems synchronizing your pom with your IDE, Stack Overflow is a better place to get a solution to your problem.

    Note that the javaee-api dependency you are using is crippled by design. These classes can be used by compilers to compile code but cannot be loaded at runtime so they cannot be used by mock or unit test frameworks. You must use real API implementations to unit test your code. See the sample code in the JSF: CDI and EL post for an example pom.

    ReplyDelete
  3. Fantastic. Just what I was looking for. I use
    @Named
    @ApplicationScoped

    For the broker and then inject it using
    @Inject
    FacesBroker facesBroker;

    Seems to be working fine.

    ReplyDelete
  4. Its very good link,could you please give any lights on how to do mock: application.getResourceBundle(facesContext, "i18n");

    ReplyDelete
  5. @Anonymous - you're really just asking how to use Mockito.

    1. Create a resource bundle using one of the ResourceBundle.getBundle methods.
    2. Use Mockito to create a mock Application instance and have it return the bundle on calls to Application.getResourceBundle
    3. Use Mockito to return the Application instance when FacesContext.getApplication is called

    ReplyDelete

All comments are moderated