Statement apply(Statement base, Description description)The first Statement parameter is a specific object which reprensents the method under the test from your test class. Such a test method can be invoked by base.evaluate(). You can place any custom code before and after the call base.evaluate(). A typically implementation follows this pattern
public Statement apply(final Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { // do something before invoking the method to be tested ... try { base.evaluate(); } finally { // do something after invoking the method to be tested ... } } }; }In short words: the apply method allows to intercept the base call of every test method and put a custom code around. Your TestRule implementation, say MyRule, can be used in any test class with the @Rule annotation as follows:
@Rule public TestRule myRule = new MyRule();Note: The member variable should be public. Let's take more examples. There is a good introduction in this tutorial. The author demonstrates how to implement two TestRules: one for SpringContext to use @Autowired in test classes and one for Mockito to populate the mocks before each test. An excellent example! I allow me to repeat the usage example.
public class FooTest { @Rule public TestRule contextRule = new SpringContextRule(new String[]{"testContext.xml"}, this); @Rule public TestRule mockRule = new MockRule(this); @Autowired public String bar; @Mock public List baz; @Test public void testBar() throws Exception { assertEquals("bar", bar); } @Test public void testBaz() throws Exception { when(baz.size()).thenReturn(2); assertEquals(2, baz.size()); } }This can not be achieved with two JUnit runners at once. E.g. you can not annotate a test class at the same time with @RunWith(Parameterized.class) and @RunWith(SpringJUnit4ClassRunner.class) or @RunWith(MockitoJUnitRunner.class).
But back to JSF. I want to show how to implement a TestRule for a simple and extensible JSF environment. First of all, we need a mock for FacesContext. We will implement it with Mockito - the most popular Java test framework. I have seen many different implementations, but in fact it is not difficult to implement a proper mock of FacesContext.
import javax.faces.context.FacesContext; import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; public abstract class FacesContextMocker extends FacesContext { private FacesContextMocker() { } 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 all PrimeFaces fan we will provide a similar mock for RequestContext.
import org.primefaces.context.RequestContext; import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; public abstract class RequestContextMocker extends RequestContext { private RequestContextMocker() { } 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 RequestContext mockRequestContext() { RequestContext context = Mockito.mock(RequestContext.class); setCurrentInstance(context); Mockito.doAnswer(RELEASE).when(context).release(); return context; } }Now, a minimal JSF / Servlet environment could be set up as follows
import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import javax.faces.application.Application; import javax.faces.component.UIViewRoot; import javax.faces.context.ExternalContext; import javax.faces.context.FacesContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.mockito.Mockito; import org.primefaces.context.RequestContext; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; public class JsfMock implements TestRule { public FacesContext mockFacesContext; public RequestContext mockRequestContext; public UIViewRoot mockViewRoot; public Application mockApplication; public ExternalContext mockExternalContext; public HttpSession mockHttpSession; public HttpServletRequest mockHttpServletRequest; public HttpServletResponse mockHttpServletResponse; @Override public Statement apply(final Statement base, final Description description) { return new Statement() { @Override public void evaluate() throws Throwable { init(); try { base.evaluate(); } finally { mockFacesContext.release(); mockRequestContext.release(); } } }; } protected void init() { mockFacesContext = FacesContextMocker.mockFacesContext(); mockRequestContext = RequestContextMocker.mockRequestContext(); mockApplication = Mockito.mock(Application.class); mockViewRoot = Mockito.mock(UIViewRoot.class); mockExternalContext = Mockito.mock(ExternalContext.class); mockHttpServletRequest = Mockito.mock(HttpServletRequest.class); mockHttpServletResponse = Mockito.mock(HttpServletResponse.class); mockHttpSession = Mockito.mock(HttpSession.class); Mockito.when(mockFacesContext.getApplication()).thenReturn(mockApplication); Mockito.when(mockApplication.getSupportedLocales()).thenReturn(createLocales().iterator()); Mockito.when(mockFacesContext.getViewRoot()).thenReturn(mockViewRoot); Mockito.when(mockViewRoot.getLocale()).thenReturn(new Locale("en")); Mockito.when(mockFacesContext.getExternalContext()).thenReturn(mockExternalContext); Mockito.when(mockExternalContext.getRequest()).thenReturn(mockHttpServletRequest); Mockito.when(mockHttpServletRequest.getSession()).thenReturn(mockHttpSession); Map<String, String> requestMap = new HashMap<String, String>(); Mockito.when(mockExternalContext.getRequestParameterMap()).thenReturn(requestMap); } private List<Locale> createLocales() { ArrayList<Locale> locales = new ArrayList<>(); locales.add(new Locale("en")); locales.add(new Locale("de")); ... return locales; } }We mocked the most used JSF / Servlet objects, linked them with each other and provided mocks via public member variables, so that they can be extended in test classes if needed. Below is an usage example which also demonstrates how to extend the mocked objects for a particular test.
public class PaymentRequestFormTest { private PaymentView paymentView; @Rule public JsfMock jsfMock = new JsfMock(); @Before public void initialize() { paymentView = mock(PaymentView.class); ... } @Test public void toJson() { // Mock URL and context path StringBuffer requestURI = new StringBuffer("http://localhost:8080/webshop"); Mockito.when(jsfMock.mockHttpServletRequest.getRequestURL()).thenReturn(requestURI); Mockito.when(jsfMock.mockHttpServletRequest.getContextPath()).thenReturn("/webshop"); // Invoke toJson method String json = PaymentRequestForm.toJson(jsfMock.mockFacesContext, paymentView); // Verify ... } }Any feedbacks are welcome.