blog.smart-java.nl
Ordina J-Technologies – Java Blog



JSF testen met EasyMock

By: Jan-Kees van Andel, 15 January 2008

Situatie

Je hoort vaak het argument dat JSF erg geschikt is voor unit testen. Meer dan bijvoorbeeld Struts. Ik denk dat dat erg meevalt en nog steeds erg afhankelijk is van hoe je je code programmeert.

Een veelgehoord argument is dat JSF Action methods, Converters, etc. geen dependencies hebben naar de Servlet API, waardoor deze goed in isolatie te testen zijn. In veel gevallen is dit argument onzin, aangezien het grootste verschil de manier is hoe je eraan komt. In veel andere frameworks worden Servlet klassen zoals HttpServletRequest en HttpServletResponse gewoon als parameter doorgegeven aan je actions, terwijl je ze in JSF opvraagt via de FacesContext verzamelbak. De dependency blijft, behalve dat hij in JSF niet expliciet wordt meegegeven.

In mijn ervaring maakt de FacesContext het zelfs lastiger om je code goed te unit testen.

  • Ten eerste is het een soort onzichtbare interface, want je applicatielogica maakt er gebruik van, maar het is niet gedefinieerd in de method signature. Wat gebeurt er als een bepaalde property (zoals de Locale of een session attribuut) null is? Er is geen fatsoenlijke manier om daar achter te komen (behalve eventuele JavaDoc of in de code spieken).
  • Bovendien heeft de klasse FacesContext een “leuke” eigenschap, namelijk dat de methode setFacesContext protected is. Dat wil dus zeggen dat je eerst een subklasse moet maken voordat je uberhaupt kunt gaan denken aan mocken. Bovendien moet je het maar net weten…

Voorbeeldje

public class LoginPageBean {         
 
  public static final String OUTCOME_LOGIN_SUCCESS = "toHomePage";         
 
  private String username, password; // + getter and setter         
 
  private LoginService loginService;         
 
  public String login() {
    boolean loggedIn = loginService.login(username, password);
    if (loggedIn) {
      return OUTCOME_LOGIN_SUCCESS;
    } else {
      FacesContext fc = FacesContext.getCurrentInstance();
      Locale locale = fc.getViewRoot().getLocale();
      String bundle = fc.getApplication().getMessageBundle();
      ResourceBundle rb = ResourceBundle.getBundle(bundle, locale);
      String msg = rb.getString("login.msg.failure");
      fc.addMessage(null,
              new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg));
      return null;
    }
  }         
 
  public void setLoginService(LoginService loginService) {
    this.loginService = loginService;
  }         
 
  // Getters and setters for username and password
}

Op zich niet zulke gekke code hè? Gewoon een klasse die een service aanroept en afhankelijk van de uitkomst een foutmelding toont.

Op het eerste gezicht ziet het er redelijk testbaar uit. Separation of concerns is redelijk. LoginService is een interface en er zit een setter voor LoginService op de klasse, dus die kan prima gemocked worden. @runtime kan de juiste service geïnjecteerd worden met JSF’s IoC mechanisme of je gebruikt een third party plug-in zoals Spring’s DelegatingVariableResolver.

Alleen out-of-the box is het niet echt geschikt om te unit testen, door die ene FacesContext. In een unit test omgeving is de currentInstance property namelijk niet gevuld en die is essentieel voor deze klasse. We halen er namelijk de Locale uit en ook de lokatie van de properties file die de meldingen bevat. Zonder currentInstance geen groene balkjes…

Oplossing met EasyMock

Eens kijken of we alle problemen kunnen oplossen en een fatsoenlijke test kunnen schrijven voor deze klasse.

Om te beginnen maken we een subklasse van FacesContext, zodat we de currentInstance kunnen zetten.

public class MockFacesContext extends FacesContext {         
 
  public static void setCurrentInstance(FacesContext fc) {
    FacesContext.setCurrentInstance(fc);
  }         
 
  /* All abstract methods are useless here,
   * so just generate them using your
   * favorite IDE and leave the stubs.
   */
}

Met deze utility klasse kunnen we de currentInstance zetten, zodat we deze straks weer netjes uit kunnen lezen.

Toen was er de JUnit test.

Let op: EasyMock ondersteunt standaard geen (abstracte) klassen zoals FacesContext, maar kan alleen interfaces mocken. Om klassen te mocke, heb je de EasyMock Class Extension nodig. Die is te downloaden van de EasyMock downloadpagina. Je hebt zowel easymock.jar en easymockclassextension.jar in je class path nodig.

public class LoginPageBeanTest extends TestCase {         
 
  private FacesContext mockFacesContext;         
 
  private Application mockApplication;         
 
  private UIViewRoot mockViewRoot;         
 
  public void testLoginValidUsernameAndPassword() {
    // Set up the objects.
    LoginPageBean bean = new LoginPageBean();
    LoginService loginService = new LoginServiceMock();
    bean.setLoginService(loginService);
    bean.setUsername("Jan-Kees");
    bean.setPassword("secret");
    // The actual method call.
    String outcome = bean.login();
    /* Ensure the correct outcome. Note I don't use constants in both the managed
     * bean and the unit test, because this is a better check. This is on purpose,
     * because it reminds you that the outcome has changed. If this test fails, you
     * are triggered to change your faces-config.xml files, because the navigation
     * rules may be incorrect.
     */
    assertEquals("loginSuccess", outcome);
  }         
 
  public void testLoginInvalidUsernameAndPassword() {
    // Set up the objects.
    LoginPageBean bean = new LoginPageBean();
    LoginService loginService = new LoginServiceMock();
    bean.setLoginService(loginService);
    bean.setUsername("invalidUsername");
    bean.setPassword("invalidPassword");
    // The actual method call.
    String outcome = bean.login();
    // Ensure the correct outcome.
    assertNull(outcome);
    // Ensure all methods (in this case, only addMessage) have been called.
    EasyMock.verify(mockFacesContext);
    EasyMock.verify(mockApplication);
    EasyMock.verify(mockViewRoot);
  }         
 
  public void setUp() {
    /*
     * Use EasyMock to create a mock FacesContext.
     * Use org.easymock.classextension.EasyMock, not the default one.
     */
    mockFacesContext = EasyMock.createMock(FacesContext.class);
    // Make it the current instance.
    FacesContextWrapper.setCurrentInstance(mockFacesContext);
    // Create a mock Application.
    mockApplication = EasyMock.createMock(Application.class);
    /*
     * With expect you can change the behavior of the object. In
     * this case we say: "The first time you call getApplication
     * on this object, it will return the following Application".
     *
     * This call to expect is necessary, because EasyMock throws
     * Exceptions if you call a method that is not "expected" to be
     * called. A way to get around this is by using EasyMock.createNiceMock.
     *
     * The anyTimes() call ensures that we can call the method more
     * than once, without risking scary EasyMock Exceptions.
     *
     */
    EasyMock.expect(mockFacesContext.getApplication()).andReturn(mockApplication).anyTimes();
    // We create a UIViewRoot mock the same way as we have created the Application mock.
    mockViewRoot = EasyMock.createMock(UIViewRoot.class);
    EasyMock.expect(mockFacesContext.getViewRoot()).andReturn(mockViewRoot).anyTimes();
    // I've only created a Dutch MessageBundle, so I create a Dutch Locale.
    Locale localeNL = new Locale("NL");
    // Whenever you call getLocale on the mock UIViewRoot, return the Dutch Locale.
    EasyMock.expect(mockViewRoot.getLocale()).andReturn(localeNL).anyTimes();
    // We have mocked the MessageBundle property of the Application.
    EasyMock.expect(mockApplication.getMessageBundle())
            .andReturn("nl.ordina.jsfsample.web.resource.Messages").anyTimes();
    /*
     * In the method under test, we call addMessage, saying:
     * "I expect you to call this method, passing null as the first
     * parameter and a FacesMessage as the second parameter"
     *
     * Note that I didn't say "anyTimes()" here. The reason is that
     * this way I am sure a message has been set. In this case, I
     * wanted to make sure the end user sees a message saying
     * something is wrong with the supplied credentials. For another
     * use case, I'd probably ignore the addMessageCall, using "anyTimes()".
     */
    mockFacesContext.addMessage((String) EasyMock.isNull(), EasyMock.isA(FacesMessage.class));
    /*
     * Because addMessage is a void method, you need expectLastCall(), instead of
     * expect(some method call). expect(void method) makes your compiler unhappy.
     */
    EasyMock.expectLastCall();
    /*
     * Before running the tests, it is important that you replay all
     * mock objects, otherwise it doesn't work. Calling replay changes
     * the state of the mock object from preparing to working.
     */
    EasyMock.replay(mockFacesContext);
    EasyMock.replay(mockApplication);
    EasyMock.replay(mockViewRoot);
  }
}

Ten slotte

Dat was een hoop boilerplate code. Vooral die setUp ziet er niet uit. Maar aan de andere kant geeft het erg veel vrijheid om je mocks te configureren. Bovendien zijn de meeste coderegels generiek voor het hele project, dus niets staat je in de weg om het weg te extraheren naar een generieke BaseTestCase.

Een paar dingen:

  • Als je EasyMock voor het eerst ziet, kan het er nogal vreemd uitzien. Vooral het concept van record-replay-verify oogt misschien tricky. Maar, als je het eenmaal te pakken hebt, wordt het vanzelf een natuurlijke manier van werken.
  • Zoals hierboven ook al staat, maak aan het begin van je project een base klasse waarvan al je UI tests overerven. Die kan dan de standaard mocks creeeren. Creatie van use case specifieke mocks en properties kun je aan de subklasse overlaten. Bijvoorbeeld met een Template Method.
  • Deze methode werkt voor vrijwel alle onderdelen van de JSF applicatie. Converters, Validators, UI Components, ActionListeners, Managed Beans, Phase Listeners, etc. zijn allemaal prima te testen. Je moet alleen opletten, want het test je code niet @runtime. In een JSF applicatie zijn veel zaken die lastiger te unit testen zijn, zoals pagina’s (eigenlijk alles wat niet in Java code geschreven is). Je hebt dus nog steeds een goede integratietest nodig. JSFUnit kun je hier goed voor gebruiken. JSFUnit test ook zaken als component tree creatie. Dit zijn zaken waar unit tests niet echt geschikt voor zijn, aangezien ze teveel afhankelijk zijn van de omgeving. Voornamelijk de verschillende expressies die je in een pagina kunt opnemen zijn lastig te mocken, zeker de verschillende foutgevallen zoals nullwaarden.
  • Ten slotte is EasyMock niet echt een black box testtool. Met EasyMock worden je tests erg afhankelijk van de exacte implementatie van je code. De volgorde van statements kan bijvoorbeeld een test breken zonder dat er functioneel iets verandert. Dit kan resulteren in vrijwel onbeheersbare situaties. Maar, als dit dreigt te gebeuren, is het sowieso misschien slim om eens naar de kwaliteit van je code te kijken.

Volgende keer staat JSFUnit op het menu.

4 reacties op “JSF testen met EasyMock”

  1. Roy van Rijn zegt:

    Goed verhaal! Mocht ik ook nog eens een project moeten doen met JSF zal ik zeker even terugkijken hier voor een goede test-opzet.

    Als we het toch over testen hebben van webframeworks heb ik nog wel een tip voor Wicket gebruikers: WicketTester.

    Deze klasse komt niet erg vaak voor in de documentatie maar het geeft de gebruiker een prima startpunt voor het testen van je Wicket applicatie. Zo verifieert deze unit test of de pagina gerenderd kan worden, of de HTML geparsed kan worden en er kunnen zelfs AJAX requests gemocked worden. Tevens is het mogelijk om via WicketTester knoppen in de drukken en de reacties in de code te bekijken. Met het gebruik van de SpringComponentInjector kunnen mock-services in the pagina geinjecteerd worden (mocht je Spring gebruiken). Voor guice is waarschijnlijk iets soortgelijks mogelijk.

  2. vlu17549 zegt:

    Ik kom er slechts een jaartje achteraf op reageren.. :)

    Een hele kleine correctie:
    /*
    * Because addMessage is a void method, you need expectLastCall(), instead of
    * expect(some method call). expect(void method) makes your compiler unhappy.
    */
    EasyMock.expectLastCall();

    Dat hoef je alleen te gebruiken als je een void-methode een exception wil laten gooien. By replay/verify wordt de call gewoon meegenomen.

    Verder duurt het verschrikkelijk lang om de FacesContext te mocken (unittest gaat van 0,07s naar 0,75s, belachelijk toch?). Ook een beetje jammer dat daar niet gewoon interfaces boven hangen, was het vast sneller gegaan…

  3. Jan-Kees van Andel zegt:

    Ja, maar het probleem is dat als ik addMessage niet expect, de test faalt omdat er een unexpected method call plaatsvindt.

    Je kunt natuurlijk ook Nice Mocks gebruiken.

  4. Peter Schuler zegt:

    Goeie post. Ik heb deze oplossing net geimplementeert voor de unittest van een custom JSF component. Wel heb je dan toch nog behoorlijke kennis nodig van de interne werking van JSF om deze componenten goed te kunnen testen. In mijn voorbeeld was het clientId noodzakelijk en heb ik mijn component als volgt geladen:

    //Maak een nieuwe ui datum object aan met een geldig id/clientId
    //en zonder renderer! Anders moet die ook nog gemocked worden.
    uiVeld = new UIVeld();
    uiVeld.setId(“jsf_12121″);
    uiVeld.setRendererType(null);

    Ik heb alleen easy mock vervangen voor Mockito (http://mockito.org/). Werkt standaard tegen classes en je hoeft geen stricte playlist meer op te geven.

Laat een reactie achter