Testning med mockobject

Idag tillhör det vanligheterna att du som utvecklare skriver automatiska test för din kod. Många projekt använder sig av olika former av dependency injection eller har beroenden mellan klasser som gör det krångligt att testa varje del för sig. Detta leder ibland till att tester omfattar stora delar att systemet (mer åt integrationstestning), vilket i sin tur leder till att det blir krångligt att underhålla och svårt att felsöka. Vissa framhåller att större tester gör dem robusta – de går inte ”sönder” lika lätt om något ändras inne i implementationen – men jag föredrar en robust implementation framför ett robust test, om ni förstår vad jag menar. Ju mindre del som varje test måste verifiera desto bättre – själva grunden för enhetstestning.

Hur bryter du isär koden så att det går att testa separat då? Jo, här kommer då mockobjekt in i bilden. Med hjälp av dessa syntetiska objekt som representerar dina kodberoenden så kan du isolera den kod som du vill testa, utan att råka ut för NullPointerException. Termen myntades (iallafall för den bredare publiken) av några utvecklare på en extreme-programming-konferens år 2000, där de också presenterade ett första ramverk för detta.

Ponera följande kodexempel (där Courier är ett interface)

class AwesomeApp {
    Courier courier;

    public Content checkPacket() {
        if (!courier.hasNewPacket()) {
            throw new NoNewPacketAsFarAsWeCouldSeeException();
        }
        Packet packet = courier.getNewPacket();
        Content content = packet.open();
        return content;
    }

    public void setCourier(Courier courier) {
        this.courier = courier;
    }
}

Om jag skriver följande test, så ramlar jag oundvikligen ned i NullPointerException-gropen eftersom courier är null i AwesomeApp.

class AwesomeAppTest {
    public void testCheckPacket() {
        AwesomeApp app = new AwesomeApp();
        Content content = app.checkPacket();
        // Assert content
    }
}

Vad göra? Lösningen är då alltså att använda ett mockobjekt. Antingen kan jag skriva en egen klass som implementerar interfacet Courier, samt hantera alla testvillkor som objekt av den klassen måste uppfylla, eller så använder jag ett färdigt ramverk. Valet känns lätt. Genom att välja ett färdigt ramverk så slipper jag en del underhåll och får samtidigt ett färdigt utvecklingsmönster att följa – annars kan det lätt bli att varje utvecklare i ett projekt gör på sitt eget sätt.

De vanligaste ramverken som används idag skulle jag säga är EasyMock, JMock samt Mockito. De är alla likvärdiga i funktionalitet och även om konstruktionerna kan skilja sig lite åt så är grunden densamma. Jag har också stött på ett ramverk som heter RMock, men det är gammalt och stöder inte ens Java 5 Generics, så jag tar inte upp det här. Här nedan följer några exempel på hur AwesomeAppTest skulle skrivas med alla tre ramverken, så att läsaren kan jämföra.

JMock

JMock har funnits länge och är en vidareutveckling av det ramverk som presenterades på extreme-programming-konferensen jag nämnde ovan. Version 1 var lite knölig att använda, eftersom metodmatchning gjordes med strängar vilket gav problem vid refaktorisering. Detta är dock löst i version 2 som sedan länge är den som gäller.

class AwesomeAppTest {
    @Test
    public void testCheckPacket() {
        // JMock-specifik klass
        Mockery mockery = new Mockery();
        // Här skapar vi vårt mockobjekt med JMock
        Courier courier = mockery.mock(Courier.class);

        // Förväntat returvärde från testade koden
        Content expectedContent = new Content();
        // Returvärden från mitt mockobjekt
        Packet anyPacket = new Packet(expectedContent);

        // Definiera våra förväntningar, ett anrop till
        // courier.hasNewPacket() och ett till
        // courier.getNewPacket() samt deras returvärden
        mockery.checking(new Expectations() {{

            oneOf(courier).hasNewPacket();
            will(returnValue(true));

            oneOf(courier).getNewPacket();
            will(returnValue(anyPacket));

        }});

        AwesomeApp app = new AwesomeApp();
        // Här sätter vi vårt mockobjekt som
        // beroende till AwesomeApp
        app.setCourier(courier);
        Content content = app.checkPacket();

        // Kontrollera att resultatet är det förväntade
        assert content == expectedContent;

        // Verifiera att förväntningarna inträffade
        mockery.assertIsSatisfied();

    }
}

EasyMock

EasyMock är också ganska gammal i gemet och var tidig med att ha stöd för refaktorisering.

class AwesomeAppTest {
    @Test
    public void testCheckPacket() {
        // Här skapar vi vårt mockobjekt med hjälp av EasyMock
        Courier courier = EasyMock.createMock(Courier.class);

        // Förväntat returvärde från testade koden
        Content expectedContent = new Content();
        // Returvärden från mitt mockobjekt
        Packet anyPacket = new Packet(expectedContent);

        // Definiera våra förväntningar, ett anrop till
        // courier.hasNewPacket() och ett till
        // courier.getNewPacket()  samt deras returvärden
        EasyMock.expect(courier.hasNewPacket()).andReturn(true);
        EasyMock.expect(courier.getNewPacket()).andReturn(anyPacket);

        // Sätt EasyMock i replay-läge,
        // där förväntningarna skall uppfyllas
        EasyMock.replay(courier);

        AwesomeApp app = new AwesomeApp();
        // Här sätter vi vårt mockobjekt som
        // beroende till AwesomeApp
        app.setCourier(courier);
        Content content = app.checkPacket();

        // Kontrollera att resultatet är det förväntade
        assert content == expectedContent;

        // Verifiera att förväntningarna inträffade
        EasyMock.verify(courier);
    }
}

Mockito

Mockito är i praktiken en vidareutveckling av EasyMock, från början en fork som nu har skrivits om helt. Tack vare arvet är dock syntaxen väldigt lik.

class AwesomeAppTest {
   @Test
   public void testCheckPacket() {
      // Här skapar vi vårt mockobjekt med hjälp av Mockito
      Courier courier = Mockito.mock(Courier.class);
 
      // Förväntat returvärde från testade koden
      Content expectedContent = new Content();
      // Returvärden från mitt mockobjekt
      Packet anyPacket = new Packet(expectedContent);
 
      // Definiera våra förväntningar, ett anrop till
      // courier.hasNewPacket() och ett till
      // courier.getNewPacket() samt deras returvärden
      Mockito.when(courier.hasNewPacket()).thenReturn(true);
      Mockito.when(courier.getNewPacket()).thenReturn(anyPacket);
 
      AwesomeApp app = new AwesomeApp();
      // Här sätter vi vårt mockobjekt som
      // beroende till AwesomeApp
      app.setCourier(courier);
      Content content = app.checkPacket();
 
      // Kontrollera att resultatet är det förväntade
      assert content == expectedContent;
 
      // Verifiera att förväntningarna inträffade
      Mockito.verify(courier);
   }
}

Summering

Som ni ser skiljer det inte mycket mellan ramverken, det som kanske framträder mest är att EasyMock har ett steg extra – där EasyMock sätts i ”replay”-läge – mot både JMock och Mockito. Alla tre har stöd för att verifiera att metodanrop sker i rätt ordning, verifiera argument etc. Det går också att sätta mjukare förväntningar, såsom att en metod skall anropas en eller flera gånger, att man inte bryr sig om anrop och returvärde på ett visst objekt osv.

Vilket ramverk du skall välja är mer upp till personlig smak än funktionalitet. I de flesta fall kanske du sitter i ett projekt som redan använder ett mockramverk och då har du förhoppningsvis fått en lite bättre förståelse för vad ramverket gör.

Som avslutning kan jag rekommendera lite läsning från utvecklarna som myntade begreppet mockobjekt. Du hittar det på www.mockobjects.com – läs gärna deras papper som ligger som länkar i högerkolumnen på den sidan.