Enhetstesting er testing av de minste enhetene i et program, dvs. teste at metodene i en klasse implementerer ønsket oppførsel. JUnit er et praktisk rammeverk for å gjøre enhetstesting av Java–klasser. Enkelt sagt består enhetstesting med JUnit, også kalt JUnit-testing, i å lage instanser av klassen som skal testes og prøve ulike sekvenser av metodekall og sjekke om verdiene de returnerer stemmer med "fasiten". La oss ta følgende Counter-klasse som eksempel.

Counter er ment å implementere en teller fra en start-verdi til (men ikke med) en slutt-verdi. Hver gang vi caller count()-metoden, så skal telleren øke med 1, men bare dersom vi ennå ikke har nådd slutt-verdien. Idet slutt-verdien nås, så skal count() returnere false, ellers true. Teller-verdien får vi tak i med getCounter()-metoden.

Første versjon av Counter-klassen
public class Counter {
   private int start, end, pos;
   public Counter(int start, int end) {
      this.start = start;
      this.end = end;
   }
   public int getCounter() {
      return pos;
   }
   public boolean count() {
      if (pos >= end) {
         return false;
      }
      pos = pos + 1;
      return true;
   }
}

Før vi tester Counter-klassen, må vi formulere oppførselen som testbare utsagn (regler) om metode-kall og returverdier. Et utsagt som at telleren skal økes hver gang count() kalles, holder ikke, siden telleren er en del av den private tilstanden til klassen og ikke en del av klassens metode-grensesnitt. Her er utsagn kun om konstruktøren og metodene:

  • Etter at klassen er instansiert med new Counter(start, end) skal getCounter() returnere start.
  • getCounter() skal returnere én mer for hver gang count() kalles, med mindre verdien er blitt end.
  • Det kallet til count() som gjør at getCounter() returnerer end, skal returnere false, ellers true.

Nå som utsagnene for oppførsel kun handler om kall til åpent tilgjengelige metoder, er det forholdsvis lett å skrive kode som tester dem. Vi instansierer rett og slett Counter-objekter og utfører sekvenser med kall og sjekker returverdier mot fasiten. JUnit-rammeverket har ferdiglagde metoder for det siste, altså å sammenligne en faktisk verdi med en forventet verdi. La oss se på testkoden for det første utsagnet over:

Testkode for konstruktøren
Counter counter = new Counter(1, 3);
assertEquals(1, counter.getCounter());	// sjekk om returverdi er 1

Counter-instansen som lages i første linje er ment å telle fra 0 til (og med) 2. Andre linje sjekker om getCounter() returnerer den forventede verdier 0. Generelt sjekker kall til assertEquals om argumentene er like, hvor det første argumentet er fasiten og det andre den faktiske (retur)verdien. For å sjekke det andre utsagnet må vi utføre et par runder med kall til getCounter() og count(), og sjekke returverdien mot fasiten for hvert kall:

Testkode for count()-metoden
assertEquals(true, counter.count());
assertEquals(2, counter.getCounter());
assertEquals(false, counter.count());
assertEquals(3, counter.getCounter());

Hvordan får vi så kjørt koden over, slik at vi får testet om Counter-koden er korrekt i henhold til kravene? Koden må først legges inn i test-metoder i en test-klasse, og så må den kjøres ved hjelp av JUnit-rammeverket. Husk å importere det du vil bruke fra

org.junit.jupiter.

Testmetodene må ha @Test over seg og ha void over seg, slik:

Test-klasse for Counter-klassen
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;


public class CounterTest {
  	@Test
	public void testCounter() {
		Counter counter = new Counter(1, 3);
		assertEquals(1, counter.getCounter()); // sjekk om returverdi er 1
		assertTrue(counter.count()); // sjekk om returverdi er true
		assertEquals(2, counter.getCounter()); // sjekk om returverdi er 2
		assertFalse(counter.count()); // sjekk om returverdi er false
		assertEquals(3, counter.getCounter()); // sjekk om returverdi er 3
	}
}

I Eclipse er det nå bare å høyreklikke på testklassen og velge Run as->JUnit Test. Da vil alle test-metodene i test-klassen bli kjørt og resultatet blir vist i et eget JUnit panel:

Meldingen forteller at sjekken vår i linje 6 i CounterTest.java har avdekket en feil, counter.getCounter() returnerte 0, mens den forventede verdier var 1! Hvis vi ser nærmere på koden, så ser vi at vi har glemt å initialisere pos-variabelen til start-verdien. Derfor startet den på 0 istedenfor 1. Dersom vi endrer linje 4 i Counter.java til pos = start; og kjører på nytt, så skal feilen være fikset:

Der avdekket vi en annen feil (programmet stopper på den første feilen i hver test-metode), i sjekken vår i linje 9 fikk vi true, men fasiten var false. Problemet denne gang er at count()-metoden returnerer true også den siste gangen den øker telleren. Vi må endre litt på logikken, slik:

Endelig versjon av Counter-klassen
public class Counter {
   private int pos, end;
   public Counter(int start, int end) {
      this.pos = start;
      this.end = end;
   }
   public int getCounter() {
      return pos;
   }
   public boolean count() {
      if (pos < end) {
         pos = pos + 1;
      }
      return pos < end;
   }
}

Denne gangen kjører testen uten feil og vi har (større) grunn til å tro at Counter-klassen er implementert i henhold til kravene.

Noen avsluttende kommentarer:

  • I dette eksemplet har vi kun testet klassen vår med ett sett test-data og dette er sjelden nok til å finne alle feil. Dersom vi f.eks. hadde instansiert med new Counter(0, 2), så hadde ikke den første feilen blitt oppdaget, siden default-verdien tilfeldigvis var riktig! Derfor er det lurt å teste med sannsynlige, usannsynlige og gjerne tilfeldige verdier.
  • Det er vanlig å strukturere koden i mange små test-metoder, som hver tester kun ett krav. I dette tilfellet kunne vi laget to test-metoder, f.eks. testCounter() for å teste konstruktøren, og testCount() for å teste count()-metoden. Ved kjøring vil begge test-metodene bli utført og vi kan avdekke flere feil på en gang. Det vil dessuten ofte være praktisk å skrive private hjelpemetoder for å gjøre test-metodene ryddigere.

 

 


Les også

4 Comments

  1. Hei Hallvard,

    Det stÅr at CounterTest mÅ arve fra TestCase, men det er ikke gjenspeilet i kildekoden. Dessuten sÅ er det vel sÅnn i JUnit4 at testmetodene annoteres med @test

  2. Unknown User (hal)

    Jeg oppdaget også det manglet extends og har rettet på det nå.

    Jeg er usikker på om vi skal gå over til JUnit4 og bruk av annotasjoner. Det er riktignok mer praktisk, men også mere "magisk". Det fine med JUnit3 er at de nesten kan nok til å forstå hvordan det virker (bortsett fra kjøringen, som bruker refleksjon). Mulig vi bruke "med JUnit3" i overskriften, og så skrive litt om JUnit4 i egne artikler.

  3. Unknown User (hal)

    Jeg skal kjøre gjennom samme eksempel og ta opp en video med Camtasia Relay, som Uninett tilbyr som en eCampus-tjeneste. Tror det er viktig med ulike former for innhold for samme stoff.

  4. Hei (smile)

    Gøy å se at det jobbes videre med dette!

    Og du må gjerne slette alle kommentarene mine på denne siden før du går "live", eller gjerne med en gang du har "gjort noe med dem".