Mock objects in Java unit tests (deel 1)

In mijn eerste blog artikel (ooit!) wil ik het graag hebben over het mocken van objecten in Java unittests.
Het is gesplitst in twee delen. In dit eerste deel zal ik kort wat highlights beschrijven van 2 Java mocking frameworks, iets vertellen over een eigen frameworkje wat ik voor dit artikel heb gemaakt, en het een en ander ook vergelijken met mocken in Groovy. In een later volgend deel kijk ik kort naar de implementatie-wijzen van de frameworks.

Inleiding

Mock objecten in de context van unit tests zijn ‘nep’-objecten die het gedrag van echte objecten simuleren wanneer een echt object (of diens gewenst gedrag) niet makkelijk beschikbaar is. De gemockte objecten worden gebruikt door echte code die getest wordt. Mock objecten kunnen op veel manieren worden aangemaakt, in de meest eenvoudige vorm is het bijvoorbeeld een String object met een inhoud die in een live systeem uit zeg een HTTPServletRequest is gedestilleerd. Zie voor meer informatie onder andere http://www.mockobjects.com/.

Aanleiding voor dit artikel is de presentatie die Peter Maas gaf over Groovy unit testing (het was geloof ik eigenlijk een presentatie over JavaOne). Er zat een voorbeeld bij dat leek op het volgende.

def request = [isUserInRole:{ 
    roleName -> assert roleName == "testRole"; 
    return true 
    }] as HttpServletRequest 
assert request.isUserInRole("testRole")

(in dit hele artikel gebruik ik een artificieel voorbeeld waarbij de test assertions doen op het mock object, dat houdt de voorbeelden wat korter)

Een HttpServletRequest object wordt in deze gemockt, waarbij het gedrag van het object in een map zit. Tijdens de presentatie werden de volgende voordelen gegeven ten opzichte van dezelfde test in Java:

  • Je hoeft niet een hele lege implementatie van de HttpServletRequest (54 methodes) op te leveren.
  • Het gedrag in de map plaatsen gaat veel sneller qua programmeren (en is leesbaarder).
  • En dit zit allemaal direct bij Groovy.

Sander’s mocking framework

Ik weet niet of ik direct naar een andere taal wil overstappen om unittests te schrijven alleen voor het mocken, maar ik zou na het Groovy voorbeeld ook wel graag mijn Java unit tests als volgt willen schrijven:

MockBehavior behavior = 
    newBehavior(). 
    addMethod("isUserInRole", Boolean.TRUE). 
    addMethod("getAttribute", 
            newBehavior().addValue("pageId", 15).
            addValue("css", "yes")); 
HttpServletRequest mockRequest = 
        createMock(HttpServletRequest.class, behavior); 
assertTrue("Basic test of mock", 
        mockRequest.isUserInRole("testRole")); 
assertEquals("Test with arguments", 15,
        mockRequest.getAttribute("pageId"));

Of toch met zoiets als

HttpServletRequest mockRequest2 =
    createMock(HttpServletRequest.class, 
        new Object() { 
            @SuppressWarnings("unused") 
            public boolean isUserInRole(String roleName) { 
                assertEquals("testRole", roleName); 
                return true; 
            } 
        }); 
assertTrue("Basic test of mock",
        mockRequest2.isUserInRole("testRole"));

als de inhoud van de te mocken methodes wat meer body zou moeten hebben.
In SimpleMock framework zit een klein framework dat deze voorbeelden ondersteund.
Groovy biedt dit overigens dus direct en met veel meer mogelijkheden (methode code direct in de map, naast interfaces ook klassen mocken, etc.).

Een echt mocking framework: EasyMock

Voor Java zijn er een aantal mocking frameworks (frameworks he, daar is de Java community sterk in…) waarvan jMock en EasyMock de bekendsten zijn. Hier is de bovenstaande test met EasyMock code:

HttpServletRequest mockRequest = EasyMock
        .createMock(HttpServletRequest.class);
EasyMock.expect(mockRequest.isUserInRole("testRole")).
        andReturn(true);
EasyMock.replay(mockRequest);
assertTrue("Basic test of mock", 
        mockRequest.isUserInRole("testRole"));
EasyMock.verify(mockRequest);

Slim aan de EasyMock syntax is dat je na het aanmaken van het object eerst de methoden aanroept die je gemockt wil hebben (daardoor krijg je code completion bij het opzetten van het mock object), waarna je replay aanroept en het mock object pas echt een mock object wordt. Daarna voer je echte tests uit, en vervolgens roep je verify aan op het mock-object om te controleren of de methoden die gepland waren om aangeroepen te worden wel echt zijn aangeroepen.

Ik zie zelf nadelen in frameworks als EasyMock:

  • De manier van werken is enorm wennen, met name dat het gedrag van aanroepen op het mockobject voor en na de ‘replay’ volledig anders is.
  • Als je voor een framework kiest zit je er behoorlijk aan vast, het is veel werk om tests om te schrijven van de syntax van EasyMock naar bijvoorbeeld jMock.
  • Het is erg gericht op het zeer precies controleren of alle methodes op het mock object wel in de juiste volgorde, met de juiste argumenten, en in de juiste hoeveelheden zijn aangeroepen. Als je een algoritme implementatie veranderd, levert dat al snel een brekende test op (hoewel het resultaat gelijk blijft).

EasyMock kan zowel voor interfaces, als met een extensie voor klassen (met limitaties op final en static methoden) mock objecten aanmaken.

Next generation mocking?: jmockit

Een nieuwer Java mocking framework is jmockit. Hier is weer hetzelfde voorbeeld, nu met jmockit code:

@Test
public void testWithJMockit() {
    HttpServletRequest mockRequest = Mockit.
            setUpMock(new JMockitHttpServletRequest());
    assertTrue("Basic test of mock",
            mockRequest.isUserInRole("testRole"));
}
 
@MockClass(realClass = HttpServletRequest.class)
public static class JMockitHttpServletRequest {
    @Mock
    public boolean isUserInRole(String roleName) {
        assertEquals("testRole", roleName);
        return true;
    }
}

JMockit heeft verschillende APIs, er is bijvoorbeeld ook een API voor ‘expectations’ vergelijkbaar met die van EasyMock.
jmockit’s implementatie werkt heel anders dan de eerdere frameworks. Dit maakt bijvoorbeeld de volgende test mogelijk (de volgende methode kan met Easymock niet zo worden getest):

// Methode die getest wordt
private boolean isNuSchrikkelJaar() {
    java.util.Date date = new java.util.Date();
    return (date.getYear() % 4 == 0);
}
 
@Test
public void testDateWithJMockit() {
    Mockit.redefineMethods(java.util.Date.class,
            JMockitDate.class);
    JMockitDate.setCurrentYear(1999);
    assertTrue("Test voor geen schrikkeljaar", 
            !isNuSchrikkelJaar());
    JMockitDate.setCurrentYear(2008);
    assertTrue("Test voor wel een schrikkeljaar", 
            isNuSchrikkelJaar());
    // Moet natuurlijk eigenlijk in teardown
    Mockit.restoreAllOriginalDefinitions();
}
 
public static class JMockitDate {
    private static int mockCurrentYear = new Date().getYear();
    static void setCurrentYear(int year) {
        mockCurrentYear = year;
    }
    public int getYear() {
        return mockCurrentYear;
    }
}

De methode die getest wordt maakt zelf een java.util.Date object aan, maar jmockit maakt het toch mogelijk het gedrag van dat object te mocken (schrikkeljaar experts hoeven overigens niet te reageren…).

Om terug te komen bij Groovy, dat heeft ook zo’n mechanisme. Maar daar werkt het wel flink wat beter. Ten eerste is de code eleganter:

void testSchrikkelJaar() {
    Date.metaClass.getYear = {-> 1999 }
    assert !isNuSchrikkelJaar()
    Date.metaClass.getYear = {-> 2008 }
    assert isNuSchrikkelJaar()
}

maar haast nog belangrijker is dat als je met jmockit standaard Java klassen wil mocken je in feite je volledige classpath moet opnemen in een -Xbootclasspath/a JVM argument, een behoorlijke ingreep in de manier waarop de testen worden gedraaid (ik zou jmockit dus denk ik alleen voor unittests gebruiken als ik geen standaard Java klassen zou willen mocken, of als het testen anders heel erg lastig zou zijn).

De volgende keer…

In het vervolg van dit blog artikel zal ik het hebben over hoe de beschreven frameworks zijn geimplementeerd. Met java.lang.reflect.Proxy is het mogelijk mock objecten voor interfaces te maken (zonder alle methoden te hoeven implementeren). Met cglib is het mogelijk dit soort proxies ook voor klassen te maken. En met ‘Java instrumentation’ is het mogelijk om zelfs hele klasse definities te overschrijven, zodat ook objecten die in te testen code worden geinstantieerd te mocken zijn.


1 reactie »

  1. Altijd leuk om te horen dat ik iemand heb kunnen inspireren! Het stukje Groovy code wat je in je eerste voorbeeld geeft is een beetje curieus. De assert hoort nmi niet thuis in het gemockte object en de return is overbodig. Ik zou dat zo schrijven:

    def request = [isUserInRole:{
        roleName ->
        roleName == "testRole"
        }] as HttpServletRequest 
    
    assert request.isUserInRole("testRole")
    

    Overigens gaat Groovy nog een stukje verder met ingebouwde mocking oplossingen, kijk bijvoorbeeld eens naar MockFor en/of StubFor (http://docs.codehaus.org/display/GROOVY/Using+MockFor+and+StubFor).

    Als we dan toch in Java willen ontwikkelen kan ik je aanraden eens de kijken naar Fest framework (http://fest.easytesting.org/) dat naast een fluent interface voor het schrijven van asserts met wat mooie templates komt voor het maken van mocks met EasyMock.

    Peter Maas - augustus 19, 2008 8:29

Reageer

RSS feed for comments on this post · TrackBack URI