2.9. Test

Kunsten at lave fejlfri programmer er svær at mestre. Kunsten at lave robuste programmer er svær at mestre. Kunsten at lave programmer der svarer til kundernes forventning er svær at mestre. Netop derfor er det nødvendigt at ethvert softwareprojekt i et eller andet omfang gennemtestes for fejl og mangler inden det sendes ud på markedet.

En modultest er en test, der afprøver en afgrænset delmængde kode i isolation fra det samlede softwaresystem. Målet er at finde fejl og sandsynliggøre at modulet fungerer efter hensigten. Når vi taler Java så er et modul som oftest enten en klasse eller en samling af klasser hørende under en fælle pakke.

I denne sektion ser vi på et yderst værdifuldt værktøj til automatiseret modultest, JUnit.

2.9.1. JUnit

JUnit er et testframework, der har til formål at lette programmørens arbejde når der skal udarbejdes testmetoder. Frameworket er centreret omkring begrebet Test Case som repræsenterer en samling af testmetoder for et enkelt modul. På dansk kaldes en test case for en testsamling.

Du kan ganske gratis hente JUnit fra http://www.junit.org. På selvsamme adresse kan du også finde mange gode artikler og eksempler. Gennemgangen i denne sektion er kun ment som en hurtig introduktion så det kan varmt anbefales selv at grave efter mere information.

2.9.1.1. Test af IntStack

I Afsnit 2.7.1 blev der defineret en stak, IntStack, der kan lagre heltal. Nedenstående kode viser et eksempel på, hvordan JUnit kan anvendes til at teste denne stak.

For at kunne oversætte og udføre eksemplet så skal jar-filen junit.jar være med i CLASSPATH.

package dk.sslug;

import junit.framework.TestCase;
import dk.sslug.IntStack;

public class TestIntStack extends TestCase
{
  public TestIntStack(String name)
  {
    super(name);
  }

  public void testPushPop()
  {
    IntStack stack = new IntStack();
    stack.push(10);
    stack.push(20);
    stack.push(30);
    assertTrue( stack.pop() == 30 );
    assertTrue( stack.pop() == 20 );
    assertTrue( stack.pop() == 10 );
  }

  public void testEmptyStackException()
  {
    IntStack stack = new IntStack();
    try {
      stack.pop();
      fail("Burde have smidt en EmptyStackException");
    } catch (java.util.EmptyStackException e) {
    }
  }
}

Der er flere ting du skal bide mærke i. For det første skal en testklassen nedarve fra TestCase-klassen. For det andet skal testmetoderne starte med navnet test for at testframeworket kan udføre dem. For det tredje skal testmetoderne være deklareret public og ikke tage imod nogen argumenter.

Metoden assertTrue(boolean) bruges til at fortælle testframeworket, hvorvidt en test skal fejle eller ej. Hvis det boolske udtryk evaluerer til falsk så fejler testen.

Der findes flere forskellige assertX()-metoder (læs dokumentationen!). De mest brugte udover assertTrue(boolean) er nok

  • assertEquals(java.lang.Object expected, java.lang.Object actual)

  • assertNotNull(java.lang.Object object)

  • assertNull(java.lang.Object object)

fail()-metoden får altid en test til at fejle.

Hvis ovenstående eksempel oversættes og køres med

[jonas@zeta eksempler/dev-env]$  java junit.textui.TestRunner \
dk.sslug.TestIntStack

så fås

.F.F
Time: 0.029
There were 2 failures:
1) testPushPop(dk.sslug.TestIntStack)junit.framework.AssertionFailedError: expected:<-1> but was:<30>
        at dk.sslug.TestIntStack.testPushPop(TestIntStack.java:27)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:42)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:28)
2) testEmptyStackException(dk.sslug.TestIntStack)junit.framework.AssertionFailedError: Burde have smidt en EmptyStackException
        at dk.sslug.TestIntStack.testEmptyStackException(TestIntStack.java:37)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:42)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:28)

FAILURES!!!
Tests run: 2,  Failures: 2,  Errors: 0

Hvis der smides en runtime exception i testmetoden som ikke fanges så fejler testen også. I JUnit-terminologi er fejl forårsaget af assertX()- og fail()-metoder failures men runtime exceptions der ikke bliver fanget er errors.

Du kan lege med at implementere IntStack i henhold til API-specifikationen og se om din implementation indeholder fejl.

Du kan også bruge en grafisk TestRunner. Hvis du kører nedenstående

[jonas@zeta eksempler/dev-env]$  java junit.awtui.TestRunner \
dk.sslug.TestIntStack

så fås

Figur 2-5. Grafisk JUnit TestRunner

2.9.1.2. Fælles initialisering for metoder

Ofte er man ude for at have to eller flere testmetoder, som skal operere på den samme kendte mængde af objekter. I JUnit-terminologi kaldes denne mængde af objekter for et test fixture og skal betragtes som en slags fast inventar som alle testmetoder kan benytte sig af. Ligeledes er det ikke sjældent at to eller flere testmetoder skal gøre brug af en fælles ressource, for eksempel en netværksforbindelse eller en databaseforbindelse.

Nedenstående eksempel viser hvordan dette kan gøres ved at overskrive metoderne setUp() og tearDown() og benytte sig af klassevariable.

package dk.sslug;

import junit.framework.TestCase;
import dk.sslug.IntStack;

public class TestIntStack extends TestCase
{
  IntStack nonempty_stack;
  IntStack empty_stack;

  public TestIntStack(String name)
  {
    super(name);
  }

  protected void setUp() throws Exception
  {
    nonempty_stack = new IntStack();
    nonempty_stack.push(10);

    empty_stack = new IntStack();
  }

  protected void tearDown() throws Exception
  {
    // intet behov for oprydning
  }

  ...
}

Hver gang en testmetode skal udføres så tages setUp() og tearDown() i brug. setUp() bliver kaldt umiddelbart før den enkelte testmetode, mens tearDown() bliver kaldt umiddelbart efter.

Bemærk at begge metoder er deklareret som protected og sat til at kunne kaste en exception.