Poster taggade med ‘test’

Sida 1/3:123

Som nyexad webbprogrammerare var det inte helt lätt att slänga sig in i certifieringsdjungeln. Det var många nya begrepp och ofta förstod jag varken frågorna eller förklaringarna när jag först började läsa. Även om jag hade haft Java som huvudspråk under min utbildningen var jag inte så van vid att prata om språket och jag hade definitivt inte ställts inför frågor om regler på hur saker och ting fungerar beroende på om man gör si eller så. Vissa klasser och metoder hade jag inte stött på tidigare och då blev de kodexempel, som egentligen skulle underlätta, ett hinder mer än ett hjälpmedel. Men skam den som ger sig… nu har jag min Java Programmer-certifiering. Det här är mina tips för hur du lyckas! Läs mer >>

I den här artikeln ska jag skriva lite om EclEmma, en plugin till Eclipse för testramverket EMMA. Pluginen är inte på något sätt ny, men inte desto mindre användbar. Verktyget är till för att visa dig hur väl dina tester täcker din kod, s. k. test coverage.

Även om man utvecklar enligt TDD (Test Driven Development) så kan det vara bra att få ett kvitto på att ens egen bild av testerna stämmer överens med verkligheten. För den som inte utvecklar testdrivet är EMMA en riktig ”eye-opener” för att, allt som oftast, inse att det behövs fler tester.

Jag fortsätter med ett exempel av Hello World-kaliber. Ladda gärna hem pluginen själv och prova samtidigt, projektets hemsida är www.eclemma.org där du kan välja manuell nerladdning eller använda länken till projektets update-site, samt att det finns tillgängligt i Eclipse Marketplace.

Då kör vi. Testet ser ut såhär:

public class EmmaTest {

 @Test
 public void testGreeter() {
    Greeter greeter = new Greeter();
    Assert.assertEquals("Hello Cygni!", greeter.greet("Cygni"));
 }
}

Och klassen vi ska testa:

public class Greeter {

 public String greet(String name) {
    if(name != null) {
       return "Hello " + name + "!";
    } else {
       return "Who are you?";
    }
 }

 public String greetSimple() {
    return "Hello you!";
 }
}

Att testet inte täcker hela Greeter-klassen har du nog redan insett. Vi låter dock EclEmma sköta analysen.

När EclEmma är installerat dyker ytterligare ett alternativ för att exekvera upp. Sedan tidigare har vi Run as och Debug as, men nu kommer även Coverage as att finnas tillgängligt. Om du kör Java-perspektivet skall också en ikon för detta ha tillkommit (kör du andra perspektiv kan du behöva lägga till ikonen via ”Customize perspective”). Vi kör testet via Coverage as:

Testet körs och vi får upp Coverage-vyn, samt att kodraderna i de klasser där det körts kod kommer att ha fått lite färg. Vi börjar med Coverage-vyn:

Som synes kan vi gå ner på olika nivåer för att tydligare se var det täcker bättre eller sämre. Efter att ha expanderat trädet fullt ut ner på metodnivå ser vi att greetSimple() har 0% täckning och att greet() behöver testas mer för att nå 100%. Metoden greetSimple() tog jag bara med här för att visa på hur användbart det är att kunna se test coverage på metodnivå och på så sätt tydligare identifiera svagheter, men jag låter den vara otestad för det här exemplet.

Om vi öppnar klassen Greeter, så ser ni tydligt vad som är testat och inte:

I greetSimple() är det inte så mycket att tala om, den är röd (no coverage) eftersom vi inte har något test som kör metoden. I greet() är raden med ”Who are you?” röd eftersom vi inte har gjort något anrop där if-satsen utvärderas till false.

Den gröna raden är grön (full coverage) därför att det är den rad som exekverades när metoden returnerade ”Hello Cygni!”, det väntade resultatet av vårt test.

Den gula raden betyder att raden är delvis täckt (partial coverage), en mycket användbar indikator. I det här fallet vet vi att det finns två scenarion för if-satsen. Antingen har argumentet name värdet null, eller så är name skilt från null. I vårt test skickade vi in ”Cygni”, name var alltså skilt från null. EclEmma talar också om för oss hur många fall vi har missat om vi håller muspekaren över diamantikonen bredvid:

Vi skriver ytterligare ett test för att nå 100% test coverage i metoden greet():

@Test
 public void testGreeterNullArg() {
    Greeter greeter = new Greeter();
    Assert.assertEquals("Who are you?", greeter.greet(null));
 }

Resultatet ser nu mycket trevligare ut:

En inställning som är bra att känna till är vilken typ av counter man kör. Hur stor test coverage dina tester har kan variera stort beroende på hur man räknar. I vårt exempel kunde vi se att vi i if-satsen hade två stycken branches. I första testet kördes bara ett anrop där uttrycket i if-satsen var sant (name var skilt från null), vilket innebar att vi täckte en branch av två – branchen där name var null kördes inte. Alltså hade vi i det testet 50% branch coverage på den raden.

Men, om vi istället räknar line coverage, så är siffran 100% eftersom line coverage inte tar hänsyn till vilken typ av kodrad som exekveras, bara att den exekveras. Klicka på den vita nedåtriktade pilen i coverage-vyn för att ändra typ av counter:

Du kan läsa mer om de olika typerna av counters som finns på EclEmma‘s hemsida eller exempelvis Wikipedia.

Två tips innan vi är färdiga:

  1. EMMA och andra liknande verktyg (Cobertura till exempel) finns som plugins till exempelvis Maven och Jenkins, som då genererar en rapport över hur testerna gick i ett bygge. Att ha statistik och rapporter över testresultat och trender i projektet ger en trevlig överblick.
  2. Om du kör ett mörkt tema för Eclipse, vilket inte är särskilt ovanligt, rekommenderar jag att ändra highlight-färgerna för EclEmma, annars är risken stor att du inte ser texten på raderna. Gå till Window -> Preferences -> General -> Editors -> Text Editors -> Annotations och välj mörkare färger för alternativen Full coverage, Partial coverage och No coverage.

Dropwizard är en körmiljö och ett ramverk för att enkelt utveckla små enheter i form av RESTful webtjänster. En stack bestående av bl.a Jetty, Jersey, Jackson, slf4j, JDBI och en föreslagen projektstruktur gör det väldigt lätt att komma igång!

Begrepp

Dropwizard har idenitifierat följande aktörer och entiteter i sin stack:

Configuration

Konfigurationen är till för att hålla miljöspecifika aspekter av tjänsten. Det kan t.ex vara databaskonfiguration eller nödvändiga sökvägar och url:ar. Konfigurationen skrivs i formatet YAML. Genom JSON-annotationer i din klass läser DropWizard automatiskt upp konfigurationen och populerar din konfigurationsklass.

Representation

Detta är de objekt som ska skickas mellan klient och server och är i sin enklaste form vanliga POJOs. Jackson används för serialisering och deserialisering. DropWizard rekommenderar att dessa objekt är immutable.

Resource

Det är resurserna som exponerar REST API:et. Varje resurs och metod har en URI och klassen annoteras med javax.ws.rs.*-typerna för att definiera sökvägar, innehållstyper och metod. DropWizard använder Jersey-implementationen.

Health check

Hälsokontroller är små kodsnuttar som testar att tjänsten är uppe och att allt är som det ska. Status för hälsokontrollerna kan inspekteras via en speciell admin-url. DropWizard skapar automatiskt några hälsokontroller, bl.a en som kontrollerar att databasanslutningen är korrekt (om man har en i sitt projekt).

Service

Servicen ansvarar för att dra igång tjänsten med en konfiguration och med resurserna initialiserade med eventuella beroenden. Detta sker genom en vanlig main-metod(public static void main(String[] args) { ... } ).

I vårt exempelprojekt ser serviceklassen ut så här:

public class MainService extends Service<MainConfiguration> {
    public static void main(String[] args) throws Exception {
        new MainService().run(args);
    }

    @Override
    public void initialize(Bootstrap<MainConfiguration> mainConfigurationBootstrap) {
        mainConfigurationBootstrap.addBundle(new AssetsBundle("/assets/", "/"));
    }

    @Override
    public void run(MainConfiguration config, Environment environment) throws Exception {

        DBI dbi = initDatabaseConnection(config, environment);

        final EventsRepository eventsRepository = dbi.onDemand(EventsRepository.class);
        final ExpensesRepository expensesRepository = dbi.onDemand(ExpensesRepository.class);

        environment.addResource(new EventsResource(eventsRepository));
        environment.addResource(new ExpensesResource(expensesRepository));
    }

    private DBI initDatabaseConnection(MainConfiguration config, Environment environment) throws ClassNotFoundException {

        final DBIFactory factory = new DBIFactory();
        final DBI jdbi = factory.build(environment, config.getDatabaseConfiguration(), "db");

        createTables(jdbi);

        return jdbi;
    }

    private void createTables(DBI dbi) {

        Handle handle = dbi.open();

        handle.execute(EventsRepository.CREATE_TABLE_STATEMENT);

        handle.execute(ExpensesRepository.CREATE_TABLE_STATEMENT);

        handle.close();
    }
}

För dig som är van vid Spring eller något annat DI-ramverk kan det kännas lite konstigt att själv behöva instansiera t.ex resursklasserna (och skjuta in Repository-implementationerna). Eftersom tanken med en DropWizard tjänst är att ansvara för en relativt liten delmängd av ett system så tycker jag inte att detta är särskilt besvärande.

Resurser, dvs REST-tjänsterna, aktiveras genom att lägga till dem till instansen av Environment. Se rad 19-20 ovan.

Komma igång

Checka ut exempelprojektet och bygg:

git clone git://github.com/cygni-stacktrace/dropwizard-sample.git
cd dropwizard-sample
mvn package

Starta tjänsten med kommandot server och filnamnet för konfigurationen:

java -jar target/expenses-1.0-SNAPSHOT.jar server development.yml

Rest-API:et ligger under kontextet /api, en GET-operation för att lista events finns t.ex här: http://localhost:8080/api/event

Admingränssnittet med länk till Health check finns här: http://localhost:8081

Testning

Testning av resurser är en enkel historia med DropWizard. Genom att låta din testklass uttöka ResourceTest kan du lägga till resursen under test så här:

    @Override
    protected void setUpResources() throws Exception {
        eventsRepository = mock(EventsRepository.class);
        addResource(new EventsResource(eventsRepository));
    }

En testmetod:

    @Test
    public void shouldShowOneEvent() throws IOException {
        //given
        Event event = new Event(1, "An event", null);

        given(eventsRepository.findById(event.getId())).willReturn(event);

        //when
        Event result = client().resource("/event/1").get(Event.class);

        //then
        assertThat("event should be returned", result, is(event));
    }

Dropwizard drar igång Jersey och invokerar anropen i testet via en HTTP-klient så hela request/response kedjan går över JSON och HTTP. Detta är bra för då testas även att Jackson kan serialisera och deserialisera dina POJO:s.

Databasstöd

Dropwizard har moduler med stöd för Hibernate och JDBI. I exempelprojektet används JDBI.

Slutsats

Det är väldigt lätt att komma igång med Dropwizard och jag tycker att de valt en vettig stack med hjälpbibliotek. Den föreslagna projektstrukturen och användandet av Maven gör att det fungerar bra i alla stora IDE:er. Att inkludera Health checks direkt från början känns väldigt bra och det är befriande att ha en app som kan köras helt stand-alone (d.v.s den behöver inte deployas i en webcontainer). Jag tycker också att de tänkt till bra kring testningen.

Om man planerar att börja använda Dropwizard i ett större sammanhang behövs dock en strategi för hur tjänster ska kunna versionshanteras samt upptäckas.

Vidare läsning

http://dropwizard.codahale.com

Detta är den sista delen av tre i en artikelserie om automatiserade integrationstester. Den första delen ger en kort beskrivning av syftet med integrationstester och de utmaningar som ofta uppstår vid testning. Där visar jag också hur man kan parallellisera tester i JUnit för att minska exekveringstiden.

Del två visar på användningen av parametriserade tester och hur man på ett bra sätt kan använda extern testdata (testfixtures).

Denna sista del visar hur man kan koppla ihop sina testsviter med open source-verktyget TestLink för att få fram trevliga rapporter. Läs mer >>

Detta är den andra delen av tre i en artikelserie om automatiserade integrationstester. Den första delen ger en kort beskrivning av syftet med integrationstester och de utmaningar som ofta uppstår vid testning. Där visar jag också hur man kan parallellisera tester i JUnit för att minska exekveringstiden.

Denna del visar på användningen av parametriserade tester och hur man på ett bra sätt kan använda extern testdata (testfixtures).

Den sista delen visar hur man kan koppla ihop sina testsviter med open source-verktyget TestLink för att få fram trevliga rapporter. Läs mer >>

När man skall skriva automatiserade integrations- eller regressionstester ställs man delvis inför lite andra utmaningar än vad som gäller för ”vanliga” enhetstester. Detta är den första delen av tre i en artikelserie om automatiserade integrationstester. Denna första del ger en kort beskrivning av syftet med integrationstester och de utmaningar som ofta uppstår vid testning. Där visar jag också hur man kan parallellisera tester i JUnit för att minska exekveringstiden.

Del två visar på användningen av parametriserade tester och hur man på ett bra sätt kan använda extern testdata (testfixtures).

Den sista delen visar hur man kan koppla ihop sina testsviter med open source-verktyget TestLink för att få fram trevliga rapporter.

Läs mer >>

När ett test skrivs för en klass som har beroenden till en datakälla, en extern service eller bara en annan klass är mockning ofta väldigt användbart. Ibland kan detta leda till att produktionskod anpassas för att det ska gå att skriva dessa tester. Nedan är exempel på ett test som testar en service som använder en entity manager.

package se.cygni.blog;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import javax.persistence.EntityManager;

import org.junit.Before;
import org.junit.Test;

public class BlogServiceTest {

    private BlogService service;
    private EntityManager entityManager;

    @Before
    public void setup() {
        entityManager = mock(EntityManager.class);
        service = new BlogService(entityManager);
    }

    @Test
    public void addEntry() {
        Entry entry = new Entry("title", "text");

        service.addEntry(entry);

        verify(entityManager).persist(entry);
    }

}

Testet i exemplet är relativt enkelt och verifierar endast att persist anropas i addEntry och ger oss följande kod för ett Spring-baserat projekt:

package se.cygni.blog;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class BlogService {

    @PersistenceContext
    private EntityManager entityManager;

    public BlogService() {
    }

    protected BlogService(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    @Transactional
    public void addEntry(Entry entry) {
        entityManager.persist(entry);
    }

}

För att kunna använda en mockad entityManager har vi lagt till en konstruktor som tar en EntityManager som BlogService sedan kan använda. Använder vi dessutom ”field injection” är vi tvugna att lägga till en no-args-konstruktor. Alternativt hade vi kunnat lägga till en setter istället.

    protected void setEntityManager(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

Det är väl antagligen inte så illa att behöva lägga till konstruktorer alternativt setters även om det blir värre när en klass har flera beroenden. Däremot känns det ju lite onödigt när både Spring och Java EE numera stödjer field injection. Det går ju att köra enhetstesterna med till exempel Spring och låta Spring injicera alla beroenden. Detta anses dock inte helt lämpligt för enhetstester utan lämpar sig bättre för integrationstester. Istället kan Mockito ta hand om injicering, genom att köra testerna med MockitoJUnitRunner och använda annotationerna @Mock och @InjectMocks. Mockito fungerar då i princip som en enkel IOC-container och några extra konstruktorer eller setters behövs inte.

package se.cygni.blog;

import static org.mockito.Mockito.verify;

import javax.persistence.EntityManager;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class BlogServiceTest {

    @Mock
    private EntityManager entityManager;

    @InjectMocks
    private BlogService service = new BlogService();

    @Test
    public void addEntry() {
        Entry entry = new Entry("title", "text");

        service.addEntry(entry);

        verify(entityManager).persist(entry);
    }
}

Efter att du har läst Martins artikel om Scala och SBT så kanske du känner dig lite sugen att börja koda i Scala, men inte riktigt vet var du skall börja. Jonas Bonér var hos Cygni och talade för ett tag sedan och nämnde då att det kan vara en bra början att skriva sina testfall i Scala för att komma igång och lära sig språket. Scala erbjuder dessutom väldigt trevliga testramverk, som kan underlätta din vardag. Jag kommer förutsätta att du idag har ett mavenprojekt med javakod och eventuella befintliga tester i java.

Första steget är att lyfta in Scalas API, JUnit 4 (om du inte redan kör det) och ett testramverk. Jag tänkte använda ScalaTest. För att göra det så lägger vi till följande beroende i pom.xml

&lt;dependency&gt;
    &lt;groupId&gt;org.scala-lang&lt;/groupId&gt;
    &lt;artifactId&gt;scala-library&lt;/artifactId&gt;
    &lt;version&gt;2.9.1&lt;/version&gt;
    &lt;scope&gt;test&lt;/scope&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
    &lt;groupId&gt;junit&lt;/groupId&gt;
    &lt;artifactId&gt;junit&lt;/artifactId&gt;
    &lt;version&gt;4.8.1&lt;/version&gt;
    &lt;scope&gt;test&lt;/scope&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.scalatest&lt;/groupId&gt;
    &lt;artifactId&gt;scalatest_2.9.1&lt;/artifactId&gt;
    &lt;version&gt;1.6.1&lt;/version&gt;
    &lt;scope&gt;test&lt;/scope&gt;
&lt;/dependency&gt;

Sedan behöver vi säga till Maven att också kompilera och köra testfall som ligger i biblioteket src/test/scala, så då lägger vi till tre plugins.

maven-scala-plugin ansvarar för att kompilera scalakoden till bytekod.

&lt;plugin&gt;
    &lt;groupId&gt;org.scala-tools&lt;/groupId&gt;
    &lt;artifactId&gt;maven-scala-plugin&lt;/artifactId&gt;
    &lt;version&gt;2.15.0&lt;/version&gt;
    &lt;executions&gt;
        &lt;execution&gt;
            &lt;goals&gt;
                &lt;goal&gt;testCompile&lt;/goal&gt;
            &lt;/goals&gt;
            &lt;configuration&gt;
                &lt;args&gt;
                    &lt;arg&gt;-make:transitivenocp&lt;/arg&gt;
                    &lt;arg&gt;-dependencyfile&lt;/arg&gt;
                    &lt;arg&gt;${project.build.directory}/.scala_dependencies&lt;/arg&gt;
                &lt;/args&gt;
            &lt;/configuration&gt;
        &lt;/execution&gt;
    &lt;/executions&gt;
&lt;/plugin&gt;

maven-surefire-plugin är den som kör testerna – om du redan har tester i projektet så har du redan lagt till den här pluginen och behöver då bara utöka den att också inkludera filer som heter **/*Spec.*.

&lt;plugin&gt;
    &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
    &lt;artifactId&gt;maven-surefire-plugin&lt;/artifactId&gt;
    &lt;version&gt;2.6&lt;/version&gt;
    &lt;configuration&gt;
        &lt;useFile&gt;false&lt;/useFile&gt;
        &lt;disableXmlReport&gt;true&lt;/disableXmlReport&gt;
        &lt;includes&gt;
            &lt;include&gt;**/*Spec.*&lt;/include&gt;
            &lt;!-- För att hantera existerande tester --&gt;
            &lt;include&gt;**/*Test.*&lt;/include&gt;
        &lt;/includes&gt;
    &lt;/configuration&gt;
&lt;/plugin&gt;

build-helper-maven-plugin lägger till källkodsmappar till kompilering, eftersom Maven från början bara hanterar en källkodsmapp för implementationskod (src/main/java som standard) och en för test (src/test/java som standard).

&lt;plugin&gt;
    &lt;groupId&gt;org.codehaus.mojo&lt;/groupId&gt;
    &lt;artifactId&gt;build-helper-maven-plugin&lt;/artifactId&gt;
    &lt;executions&gt;
        &lt;execution&gt;
            &lt;id&gt;generate-test-sources&lt;/id&gt;
            &lt;phase&gt;generate-test-sources&lt;/phase&gt;
            &lt;goals&gt;
                &lt;goal&gt;add-test-source&lt;/goal&gt;
            &lt;/goals&gt;
            &lt;configuration&gt;
                &lt;sources&gt;
                    &lt;source&gt;src/test/scala&lt;/source&gt;
                &lt;/sources&gt;
            &lt;/configuration&gt;
        &lt;/execution&gt;
    &lt;/executions&gt;
&lt;/plugin&gt;

Ok, nu behöver vi bara skapa mappen src/test/scala och sedan kan du börja koda!

Om du vill kan du stanna här. Nu kan du skriva testklasser i Scala, annotera metoderna med JUnits @Test och köra på som vanligt. Det du vinner är att du får tillgång till Scalas syntax och konstruktioner och att du kan lära dig i en bekant miljö.

Det andra alternativet är att du också börjar titta på vad Scalas testramverk kan erbjuda. I grunden är det inte så stor skillnad, men rent syntatiskt och kanske också tankemässigt så är det lite annorlunda. ScalaTest har stöd för BDD – Behaviour Driven Development. Tanken är att varje test är en beskrivning av en funktion/ett beteende som en roll skall uppfylla. Rollen tar sedan formen av en eller möjligtvis flera klasser. Tack vare att Scalas syntax går att det skriva ganska koncisa specifikationer, nästan som rena texttester för att beskriva beteendet som förväntas.

Vi börjar med att skriva en första specifikation, som vår applikation skall uppfylla.

@RunWith(classOf[JUnitRunner])
class MyApplicationSpec extends FlatSpec with MustMatchers {

  &quot;My application&quot; must &quot;convert a string to hex values&quot; in {
  }

}

Det här är ett komplett test, även om det inte gör något just nu – kroppen på specifikationen är tom. Om jag vill kan jag lämna det tomt och fylla på senare, eller så gör jag det lite tydligare att jag inte är klar med testet. Då skriver vi om det såhär

@RunWith(classOf[JUnitRunner])
class MyApplicationSpec extends FlatSpec with MustMatchers {

  &quot;My application&quot; must &quot;convert a string to hex values&quot; in (pending)

}

Väldigt tydligt och förklarande, eller hur? Ponera att jag skapar en klass som kan konvertera en sträng till hexadecimala värden (wow!). Då kan testkoden se ut som följande

@RunWith(classOf[JUnitRunner])
class MyApplicationSpec extends FlatSpec with MustMatchers {

  &quot;My application&quot; must &quot;convert a string to hex values&quot; in {
    val app = new MyApplication()
    app.convert(&quot;The donkey makes a left turn. I observe.&quot;) must equal (&quot;54686520646f6e6b6579206d616b65732061206c656674207475726e2e2049206f6273657276652e&quot;)
  }

}

Klassen MyApplication är fortfarande skriven i Java, men jag testar den från Scala. Nyckelordet ”must” kommer från en trait i ScalaTest som heter MustMatchers. Det går också att använda ShouldMatchers, enda skillnaden är terminologin – must eller should, vilket du tycker om bäst.

Genom att fylla på med test av det här slaget så får du en ganska tydligt dokumentation vad du faktiskt förväntar dig av dina roller/klasser. Då slipper du en massa kommentarer i koden och/eller testmetoder som döpts ”testShouldConvertStringToHexValue” eller liknande. Enkelt och snyggt!

Vill du använda mockobjekt i din scalakod så kan du antingen köra på någon av de vanliga javabiblioteken (JMock, EasyMock, Mockito), eller så kan du prova Borachio som är skrivet i Scala.

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.

Du har just klickat dig in på den första delen i min samling artiklar om Python – detta dynamiska högnivåspråk som både jag, många andra utvecklare samt organisationer som Google och YouTube tycker är grymt att utveckla i. I de kommande artiklarna tar jag upp bakomliggande filosofier, egenskaper hos språket, hantering av datatyper och allmänna tips och tricks som kan vara till hjälp för dig som är ny i språket. Läs mer >>

Sida 1/3:123