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).
- Understanding the Lifecycle of the
FacesContext
- Mocking for
FacesContext.getCurrentInstance()
- Designing for Test with Dependency
Injection
- Dependency Injection and Scopes
- 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 FacesContext
s 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 = (Integer) session.get("foo");
foo = (foo == null) ? 1 : 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 invocation) throws 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 = (Integer) session.get("foo");
foo = (foo == null) ? 1 : 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 = (Integer) session.get("foo");
foo = (foo == null) ? 1 : 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
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;"
ReplyDeletei 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.
"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.
ReplyDeleteNote 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.
Fantastic. Just what I was looking for. I use
ReplyDelete@Named
@ApplicationScoped
For the broker and then inject it using
@Inject
FacesBroker facesBroker;
Seems to be working fine.
Its very good link,could you please give any lights on how to do mock: application.getResourceBundle(facesContext, "i18n");
ReplyDelete@Anonymous - you're really just asking how to use Mockito.
ReplyDelete1. 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