© 1999-2010, Flemming Koch Jensen
Alle rettigheder forbeholdt
JUnit

Opgaver

 

 

Bemærk: Dette kapitel beskriver hvordan JUnit anvendes i forbindelse med Eclipse. JUnit kan også anvendes sammen med andre udviklingsmiljøer, men dette er ikke beskrevet.

 

Framework JUnit er et framework, der understøtter test af komponenter ("units"). Det er udviklet af Erich Gamma and Kent Beck. Erich Gamma er kendt fra GoF ("Gang of Four"), der er "øgenavnet" for de fire forfattere til den skelsættende bog. "Design Patterns", der for alvor satte gang i pattern-bølgen i midten af 90'erne. Kent Beck er kendt som manden bag extreme programming, hvor netop regressiv test af komponenter spiller en central rolle.
Regressiv test Idéen med regressiv (dk.: gentagen) test er, at man til hver en tid kan køre samtlige tests igen, og dermed sikre sig at man ikke indfører fejl i noget man allerede har fået til at virke. Regressiv test giver mange fordele, men man skal hele tiden huske at fordelene ikke er bedre end de tests man laver. Som bekendt kan ingen test garantere fuldstændig mod fejl, og derfor er fordelene ikke absolutte. Regressiv test giver en betydelig større tryghed når man laver refactoring, man kan efter en refactoring gentage ens tests, og på den måde skabe en større sikkerhed for at man ved refactoring ikke har indført nye fejl. Refactorings største udfordring er nemlig: "If it works, don't fix it". Denne sætning er sund i sig selv, men når vi laver refactoring er det fordi vi ikke vil nøjes med at det virker, vi vil også have et sundt design, der er til at arbejde videre med.
Forsømt område Det største problem med tests er at de i almindelighed har så dårlige vilkår. Det er ofte noget udviklerne laver sidst på natten før systemet skal leveres. JUnit har været med til at gøre opbygningen af en samling af test cases lettere og dermed mere spiselig. På den måde har extreme programming høstet positive resultater, fordi man har formået at give tests en styrket rolle i processen.
Der er dem der mener at det ligefrem bliver sjovt at lave tests når man bruger JUnit, men det er nok en overdrivelse — der er det, der er sjovere! Til gengæld kan man sige at smerten ikke længere er så stor, og det er bestemt et gode, for test har altid haft ry for at være kedeligt — og været det!

 

1. Installation

Man kan downloade den seneste version af JUnit fra www.junit.org. Her finder man også links til artikler oa. der beskriver JUnit. Der er dog visse af disse der er lidt ensidige, idet de er tæt på at betegne JUnit som alle problemers løsning!
jar-file I forbindelse med den, eller de filer, man downloader er der en JAR-file: junit.jar, (der indgår normalt et versions-nummer i dette filenavn, f.eks.: junit-4.8.1.jar) som indeholder frameworket. Da man sikkert (forhåbenligt) vil anvende JUnit i mange af sine projekter, er det bedst at placere denne file i JRE's extension directory (alt efter Java's versions-nummer, kan dette directory f.eks. hedde: C:\Program Files\Java\jre6\lib\ext), så den er til rådighed for alle Java programmer (det gælder også Eclipse).
Eclipse Hvis man anvender Eclipse skal man desuden være opmærksom på: "Access Rules"-problemet, i forbindelse med jar-filer.

 

2. Et simpelt eksempel

Lad os starte med et meget simpelt eksempel, hvor vi tester en klasse, der er så enkel, at vi alene kan koncentere os om hvordan vi anvender JUnit.
Kildetekst 1:
Simpel anvendelse af JUnit
Heltal.java
public class Heltal {
  private int tal;
  
  public Heltal() {
    this( 0 );
  }
  
  public Heltal( int tal ) {
    set( tal );
  }
  
  public void set( int tal ) {
    this.tal = tal;
  }
  
  public int get() {
    return tal;
  }
}
HeltalTest.java
import org.junit.*;
import static org.junit.Assert.*;

public class HeltalTest {
  
  @Test
  public void testKonstruktor() {
    Heltal t = new Heltal( 8 );
    
    assertEquals( 8, t.get() );
  }
  
  @Test
  public void testSet() {
    Heltal t = new Heltal();
    t.set( 5 );
    
    assertEquals( 5, t.get() );
  }
}
Først har vi klassen: Heltal som vi vil gøre til genstand for test. Det er en triviel integer wrapper.
Dernæst har vi selve test-klassen: HeltalTest, denne klasse har til formål at teste Heltal-klassen. Der er et par ting at bemærke i forbindelse med denne klasse.
@Test Klassen har ingen konstruktor, men til gengæld to metoder. Disse to metoder udgør to test cases. Dette markeres overfor JUnit med annotationen: @Test. Denne annotation er, sammen med flere vi skal se i det følgende, erklæret i package: org.junit, som vi af samme grund importerer. Denne package stammer fra den jar-file vi "installerede" ovenfor. At man anfører denne annotation, gør det muligt også at have service-metoder i klassen, der ikke selv er test cases, men kan anvendes at forskellige test cases, der måtte være i klassen. Disse service-metoder vil ikke have nævnte annotation.
Assert-
metoderne
En anden ting man bemærker, er kaldene af assertEquals-metoden. Vi skal senere se en lang række af disse assert-metoder, men man kunne indledningsvis undre sig over, hvor denne metode er erklæret, da HeltalTest-klassen tydeligvis ikke arver denne metode (den nedarver jo fra Object). Assert-metoderne stammer fra klassen: Assert, der også er erklæret i package: org.junit. Ved at anføre en statisk import af denne klasse, bliver alle (public) statiske metoder i denne klasse tilgængelige, og dermed også assertEquals-metoden, uden at vi ved anvendelsen skal referere til Assert-klassen.
Alternativt til denne statiske import, kunne man have kaldt metoderne med, f.eks.:
Assert.assertEquals( 8, t.get() );
assertEquals-kaldene er vore to test-metoders, eller test cases', måde at kontrollere om henholdsvis konstruktoren og set-metoden i Heltal-klassen fungerer korrekt. Metoden sammenligner det forventede resultat (8 og 5) med det observerede, der i begge tilfælde hentes med get-metoden. Denne sammenligning er i sig selv ikke noget særligt, den er faktisk gange banal, men hvis sammenligningen ikke falder gunstigt ud opfanger JUnit dette, og vi får besked om at en eller flere at vores test cases har fejlet. (Rent teknisk sker dette ved at assert-metoderne kaster en exception.)

De enkelte test cases er uafhængige af hinanden, og rækkefølgen de udføres i er udefineret. Når man laver en test-metode, til en test case, skal man derfor starte forfra som vi ser det gjort i begge metoder, ved at de laver en instans af Heltal, som eftefølgende anvendes.

 

Vi har nu gennemgået de specielle ting omkring HeltalTest-klassen, men hvad gør man for at få udført testen med de to test cases?

Ctrl-F11 I Eclipse kan det gøres på flere måde, men den simpleste er at aktivere det editor vindue i Eclipse, der indeholder test klassen, og køre den med Ctrl-F11. Man kan også vælge at højreklippe på klassen i package explorer, og vælge Run as ... JUnit Test. Ønsker man kun at køre en enkelt test case, kan dette gøres ved at place cursoren i navnet på test-metoden, f.eks.: testKonstruktor, og trykke Ctrl-F11.
Kører man testen i HeltalTest-klassen (begge test cases), får man et specielt JUnit panel, der har følgende udseende:
Figur 1:
JUnit i Eclipse
JUnit i Eclipse
Her er panelet samlet med package panelet med tabs, men det kan man selv vælge ved at drag'e det rundt i Eclipse.
Expand All En anden ting er, at der ovenfor er højreklikket på: HeltalTest... (det markerede) og valgt: Expand All. Hvis der ikke er nogen fejl i test-kørslen (hvilket der ikke er med vores to test cases) vil den pågældende knude i træet være kolapset, og vi kan ikke se de enkelte test cases' resultat.
Hvis der havde været en fejl i forbindelse med den test case, der testede set-konstruktoren (f.eks. ved at den altid ville sætte datakernen til 14), ville det i stedet have set ud som følger:
Figur 2:
Use case der fejler
Failed Test Case

Her er træet automatisk expanded, og vi kan se hvilke test cases der har fejlet. Denne gang har vi også taget den nederste del af panelet med; hvor en fejlmeddelelse fortæller at den observerede værdi: 14, ikke svarer til den forventede værdi: 8. Den anden linie fortæller hvor i test-metoden fejlen opstod. Man kan klikke på denne linie og automatisk komme til dette sted i kildeteksten.

Rerun Test

Efterhånden som man retter fejl (eller forsøger på det) kan man køre testen igen ved at trykke på Rerun Test ikonet (det er det grønne ikon med den hvide trekant øverst i JUnit panelet).

Errors og
failures

Skulle der opstå en fejl i forbindelse med en test case, der ligger udenfor rammerne af JUnit (f.eks. at der opstår en uventet null pointer exception) vil dette blive registreret for sig, som en error. Som man kan se i panelet skelner JUnit mellem failures og errors, og denne skelnen ligger i om fejlen er en (forventet) fejl i en test case (en failure), eller en (uventet) fejl (en error). En uventet fejl ville f.eks. være hvis vores test af konstruktoren gav en null pointer exception; hvilket ikke ville være den form for fejl vi havde forventet, idet vi tester for om den rigtige værdi bliver sat. En uventet fejl er rent teknisk, at der bliver kastet en exception, der ikke er en instans af JUnits egne exception klasser.

 

3. assert-metoder

Vi har ovenfor brugt assertEquals-metoden, men der findes en lang række af assert-metoder, som vi kort vil gennemgå i det følgende:
Først har vi assert-metoden vi netop har brugt:
void assertEquals( ... , ... )
assertEquals-metoden er overloaded i et utal af varianter, alt efter parametrenes type. Man kan sammenligne værdier af næsten alle de primitive typer, samt objekter.
Alle varianter af assertEquals-metoden, har en ekstra variant tilknyttet, der som første parameter tager en tekststreng, der anvendes hvis testen fejler. Dette gør sig generelt gældende for alle assert-metoder, og det vil derfor ikke blive gentaget i det følgende.
En anden meget brugt assert-metode er:
void assertTrue( boolean assertion )
void assertTrue( String message, boolean assertion )
Den bruges når man formulerer en assertion, i form af et boolsk udtryk.
Til at checke om en reference er null eller ej, har man:
void assertNotNull( Object ref )
void assertNotNull( String message, Object ref )
void assertNull( Object ref )
void assertNull( String message, Object ref )
Hvis det forventede output skal være null eller forskelligt fra null, kan disse assert-metoder anvendes. Bemærk at dette ikke forhindrer test casen i efterfølgende at anvende det returnerede, såfremt det var forskellig fra null.
En anden assert-metode, der ser på referencer er følgende:
void assertSame( Object forventet, Object observeret )
void assertSame( String message, Object forventet, Object observeret )
Her sammenlignes de to referencer med hensyn til reference-lighed, dvs. refererer forventet og observeret til det samme objekt?
Følgende metode er egentlig ikke en rigtig assert-metode, da den altid fejler, men det vil alligevel være naturligt at nævne den her:
void fail()
void fail( String message )
Idéen med den er, at hvis test casen når til det sted i kildeteksten, hvor dette kald af fail er placeret, så er der noget galt!

 

4. Test opsætning

I vores eksempel overfor have vi to test cases, der begge anvendte en instans af Heltal. For den førstes vedkommende skulle datakernen sættes til 8 af set-konstruktoren, mens den anden for så vidt var uinteresseret i objektets initielle tilstand, da et kald af set-metoden skulle testes. I situationer, hvor en række test cases har behov for det samme udgangspunkt, kunne man ønske at lave en fælles initialisering til dem. Den kunne i vores eksempel være det at lave en instans af Heltal, der fik sat sin datakerne til 8 af set-konstruktoren. JUnit understøtter, at man kan erklære en sådan fælles initialiserings-metode for de test cases, der er i den samme klasse.
I vores eksempel, kunne det gøres på følgende måde:
Kildetekst 2:
Test opsætning
HeltalTest.java
import org.junit.*;
import static org.junit.Assert.*;

public class HeltalTest {
  private Heltal t;
  
  @Before
  public void setUp() {
    t = new Heltal( 8 );
  }
  
  @After
  public void tearDown() {
    t = null;
  }
  
  @Test
  public void testKonstruktor() {
    assertEquals( 8, t.get() );
  }
  
  @Test
  public void testSet() {
    t.set( 5 );
    
    assertEquals( 5, t.get() );
  }
}
Vi har gjort instansen af Heltal tilgængelig vha. en instansvariabel, så alle test cases kan deles om den. Før udførelsen af hver af de to test cases, vil JUnit kalde setUp-metoden, så der er gjort klar til testen. Bemærk, at ingen af de to test cases selv gør noget klar. Grunden til at setUp-metoden bliver kaldt, er at den er annoteret med @Before. Metoden kunne udemærket have et andet navn, men navnet: setUp, er mere eller mindre en konvention, da det i tidligere versioner af JUnit har været et krav at metoden skulle havde dette navn.
Udfaktorisere setUp-metoden behøver naturligvis ikke gøre alt klar til en given test. Hvis test casene i en test-klasse kun har visse dele af klargøringen til fælles, kan man blot udfaktorisere det pågældende, og lade resten stå i de enkelte test cases. Af samme grund er setUp-metoden ikke så speciel som man umiddelbart kunne tro, da en udfaktorisering af fælles initialiseringskode også kunne ske ved et manuelt kald af en sådan metode i starten af hver test case — en løsning som man ville vælge hvis kun en del af test casene havde en fælles initialisering.
Rydde op Det er ikke al test-opsætning, der blot består i at gøre en række objekter klar. Det kan f.eks. også dreje sig om at etablere en netværksforbindelse. I så fald har vi også brug for at rydde op efter hver test (e.g. lukke en netværksforbindelse). Dette kan tilsvarende gøres ved at annotere en metode med: @After. I eksemplet ovenfor, har vi ikke noget reelt behov for en sådan metode, og dens indhold er derfor også banalt, idet den blot overlader Heltal-objektet til garbage collectoren, ved at droppe referencen til den — noget der alligevel ville ske ved næste kalde af setUp-metoden. Det er sædvane, at man kalder denne metode: tearDown, selvom det igen er valgfrit, og det igen skyldes at det tidligere har været påkrævet at man anvendte dette navn.
Ligesom setUp-metoden kaldes før hver enkelt test case, således kaldes tearDown-metoden også efter hver enkelt test case er blevet udført.

 

5. Test suites

Samling Har man mange tests, der er placeret i adskillige test-klasser, kan det blive en udfordring at holde styr på dem - at organisere dem. Til det formål anvender man test suites ("suite" udtales: swiit). En test suite er i JUnit en klasse, der samler en række klasser med test cases. Det betyder at en test suite også kan bestå af andre test suites (i tråd med Composite Pattern).
I vores eksempel bliver det dog ikke så avanceret, med mindre vi begynder at lave mange test cases. Vi vil derfor nøjes med at dele de to test cases, vi har lavet, ud på to test-klasser, og dernæst samle dem i én test suite:
Kildetekst 3:
Simpel anvendelse af JUnit
HeltalTest1.java
import org.junit.*;
import static org.junit.Assert.*;

public class HeltalTest1 {
  
  @Test
  public void testKonstruktor() {
    Heltal t = new Heltal( 8 );
    
    assertEquals( 8, t.get() );
  }
}
HeltalTest2.java
import org.junit.*;
import static org.junit.Assert.*;

public class HeltalTest2 {
  
  @Test
  public void testSet() {
    Heltal t = new Heltal();
    t.set( 5 );
    
    assertEquals( 5, t.get() );
  }
}
SamletTest.java
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
 
@RunWith( Suite.class )

@Suite.SuiteClasses( {
  HeltalTest1.class,
  HeltalTest2.class
} )

public class SamletTest {
}
Grimt! Som det ses, er syntaksen omkring test suites ikke nogen skønheds-åbenbaring, men det virker!
Vil man kører en sådan test suite i Eclipse, gøres det fuldstændig tilsvarende som at køre en test-klasse.

 

6. Exceptions

Hvis man vil teste om en exception bliver kastet i den rigtige situation, kan det gøres på følgende måde:
Kildetekst 4:
Test af exception
UpsException.java
public class UpsException extends Exception {
  
  public UpsException( String s ) {
    super( s );
  }
}
Exceptional.java
public class Exceptional {
  
  public void go() throws UpsException {
    // throw new UpsException( "Oh, dear!" );
  }
}
ExceptionalTest.java
import org.junit.*;

public class ExceptionalTest {

  @Test( expected=UpsException.class )
  public void testUpsException() throws UpsException {
    Exceptional exceptional = new Exceptional();
    
    exceptional.go();
  }
}
Forglemmelse Vi har lavet en simpel exception: UpsException, og en klasse: Exceptional, med en simpel metode: go, der skal kaste en UpsException — men det gør den ikke! Vi har nemlig (midlertidigt) udkommenteret linien, og glemt af indkommentere den igen.
I forbindelse med eksemplet ovenfor, meddeles fejlen i JUnit på følgende måde:
Figur 3:
Test af exception
Exception failed