JUnit

Sven-Olof Nyström
OOP med Java våren -25
Informationsteknologi
Uppsala Universitet

Unit testing

Normalt testar vi bara ett program i sin helhet. Det gör att det ibland kan ta lång tid att se om en bit kod vi skrivit verkligen fungerar som den ska. Idén med unit testing är provköra enstaka klasser under utvecklingen, och avsikten är att alla unit tests ska lyckas efter varje ändring i programmet. Unit tests kan utföras automatiskt, det är därför lätt att köra dem regelbundet under ett större projekt. Eftersom varje komponent (en klass, till exempel) har sina egna unit tests är det lätt att se var ett fel har uppstått.

Unit testing är särskilt värdefullt för komponenter som är komplexa och svåra att testa när de integrerats i en större applikation. Lite paradoxalt kan unit tests göra det lättare att göra ändringar i programmet. Det kan ofta vara svårt att försäkra sig om att ett komplicerat program fungerar som det ska efter en ändring men unit testing hjälper oss att automatisera testningen. Det blir därför lättare att känna sig säker på att en ändring inte förstör nånting.

(Å andra sidan kan ändringar av gränssnittet till en klass kräva att man ändrar ett stort antal unit tests.)

Man kan använda unit tests som ett sätt att specificera vad klassen ska göra. Då börjar man alltså med att skriva testerna. Unit tests kan också fungera som dokumentation om hur en klass är avsedd att användas.

Det finns många som förespråkar att man skriver unit tests för alla metoder i alla klasser. Jag kan se poängen med att göra så, men det blir mycket kod.

Du kan också lägga till unit tests vid debugging: om du hittar en bug som inte fångas upp av något befintligt test, skapa (och lägg till) ett unit test som fångar buggen. Sedan fixar du buggen och kollar att programmet klarar alla unit tests, både de som fanns sen tidigare och det du lagt till. Detta bör garantera att buggen omedelbart upptäcks om den skulle återkomma (det händer ofta att fixade buggar dyker upp igen).

JUnit

(Användning av JUnit ingår inte som ett obligatoriskt moment i kursen.)

Man kan skriva unit tests utan något särskilt verktyg, men om man använder ett ramverk som JUnit blir det lättare att skriva och köra testerna.

JUnit's hemsida är här: http://www.junit.org/. Där finns systemet för nerladdning samt diverse dokumentation. Se även länkar på länksidan.

Låt oss betrakta ett exempel på en klassdefinition i Java.

Exempel: Person

Vi tar en variation på klassen Person som visats tidigare. Här har jag lagt till en metod birthday() som ökar personens ålder med 1.

class Person {
    private String name;
    private int age;

    public String getName () {
        return name;
    }

    public void setName(String name) {
        name = name;
    }

    public int getAge() {

        return age;
    }

    public void setAge(int a) {
        age = a;
    }

    public void birthday() {
        age = age + 1;
    }
}

Naturligtvis har jag inget sätt att testköra klassen.

Lösning: skriv några unit test.

Följande kod ligger i filen PersonTest.java.

import org.junit.*;
import static org.junit.Assert.* ;

public class PersonTest {

   @Test
   public void testAge() {
      System.out.println("Test if getAge and setAge work...") ;
      Person p = new Person();
      p.setAge(42);
      assertTrue(p.getAge() == 42) ;
   }

   @Test
   public void testName() {
      System.out.println("Test if getName and setName work...") ;
      Person p = new Person();
      p.setName("Kalle");
      assertTrue(p.getName() == "Kalle") ;
   }

   @Test
   public void testBirthday() {
      System.out.println("Test if birthday works...") ;
      Person p = new Person();
      p.setAge(10);
      p.birthday();
      assertTrue(p.getAge() == 11) ;
   }
}

Filen är (i stort) som en vanlig Java-fil. Jag importerar klasser och metoder ur ett bibliotek som jag laddat ner från http://www.junit.org/.

Sen följer tre unit tests för olika aspekter av klassen Person. testAge och testName kollar att motsvarande get- och setmetoder fungerar som väntat. testBirthday kollar att man verkligen blir ett år äldre på sin födelsedag.

Låt oss testköra. Vid testkörningen använder jag en jar-fil med namnet junit-4.6.jar som jag laddat ner från http://www.junit.org/.

Vi inleder med att kompilera filen Person.java samt testfilen PersonTest.java.

$ javac Person.java
$ javac -cp junit-4.6.jar:. PersonTest.java

Flaggan -cp junit-4.6.jar:. säger åt kompilatorn att även leta efter klasser i filen junit-4.6.jar.

Efter kompileringen är det dags att kolla om programmet klarar testerna. Notera att vi kör programmet JUnitCOre med PersonTest som parameter.

$ java -cp junit-4.6.jar:. org.junit.runner.JUnitCore PersonTest

Detta ger följande (ganska långa) utskrift:

JUnit version 4.6
.Test if getAge and setAge work...
.Test if getName and setName work...
E.Test if birthday works...

Time: 0.005
There was 1 failure:
1) testName(PersonTest)
java.lang.AssertionError:
	at org.junit.Assert.fail(Assert.java:92)
	at org.junit.Assert.assertTrue(Assert.java:44)
	at org.junit.Assert.assertTrue(Assert.java:55)
	at PersonTest.testName(PersonTest.java:20)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
	at java.lang.reflect.Method.invoke(Method.java:597)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
	at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:31)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:44)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:180)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:41)
	at org.junit.runners.ParentRunner$1.evaluate(ParentRunner.java:173)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
	at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:31)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:220)
	at org.junit.runners.Suite.runChild(Suite.java:117)
	at org.junit.runners.Suite.runChild(Suite.java:24)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:180)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:41)
	at org.junit.runners.ParentRunner$1.evaluate(ParentRunner.java:173)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
	at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:31)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:220)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:159)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:138)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:119)
	at org.junit.runner.JUnitCore.runMain(JUnitCore.java:100)
	at org.junit.runner.JUnitCore.runMainAndExit(JUnitCore.java:54)
	at org.junit.runner.JUnitCore.main(JUnitCore.java:46)

FAILURES!!!
Tests run: 3,  Failures: 1

Som ni ser misslyckas testName. Om man tänker efter lite är det inte så konstigt. Så här ser metoden setName ut:

public void setName(String name) {
    name = name;
}

Eftersom parametern och instansvariabeln heter samma sak fungerar inte metoden som det var tänkt.

Om vi reparerar metoden...

public void setName(String name) {
    this.name = name;
}

...och testar igen...

$ javac Person.java
$ java -cp junit-4.6.jar:. org.junit.runner.JUnitCore PersonTest
JUnit version 4.6
.Test if getAge and setAge work...
.Test if getName and setName work...
.Test if birthday works...

Time: 0.004

OK (3 tests)

...lyckas alla tester.

Exempel: Bicyclist

Vi utökar exemplet ovan med en klass Bicycle. Precis som i ett tidigare exempel låter vi cykelns fart bero av cyklistens ålder. (Jag har valt exemplet för att det illustrerar hur egenskaper hos ett objekt beror av ett annat objekt.)

public class Bicycle {
    private Person cyclist;
    Bicycle (Person p) {
        cyclist = p;
    }
    double getSpeed () {
        if (cyclist.getAge() > 60) return 10;
        if (cyclist.getAge() > 25) return 20;
        return 30; //Larvigt...
    }
}

Unit test för denna klass kan se ut så:

import org.junit.*;
import static org.junit.Assert.* ;

public class BicycleTest {

    @Test
    public void testYoung() {
        System.out.println("Test if young person can go fast...") ;
        Person p = new Person();
        p.setAge(0);
        Bicycle b = new Bicycle(p);
        assertTrue(b.getSpeed() == 30) ;
    }

    @Test
    public void testBirthday() {
        System.out.println("Test if person slows down as he becomes older...") ;
        Person p = new Person();
        p.setAge(25);
        Bicycle b = new Bicycle(p);
        double v = b.getSpeed();
        p.birthday();
        assertTrue(b.getSpeed() < v) ;
    }
}

Det första testet kollar om en ung person verkligen kan cykla snabbt, det andra kollar om en person blir långsammare när han (eller hon) blir äldre.

Notera att vi var tvungna att skapa ett objekt ur klassen Person för att testa klassen Bicycle. I detta fallet gjorde det inte så mycket—klassen Person är ju ganska enkel. I andra fall kan det vara svårare att sätta upp testet. Ett objekt ur en klass man vill testa kan tex referera till klasser som kommunicerar med användaren eller med databaser.

Detta kan lösas genom att man definierar nya klasser som implementerar de externa klassernas gränssnitt. I exemplet Bicycle skulle kunna man ha en klass MockPerson (säg) som hade samma gränssnitt som Person.

Avslutning

Ni hittar mer information om JUnit på JUnits sida.

Naturligtvis kan man även skriva unit tests utan nåt särskilt verktyg.

Notera också att det lätt blir mycket tester. Även för den enkla klassen i exemplet blev det en hel del kod.