Parametrisiertes Testen mit JUnit

Martin Kompf

JUnit 4 erweitert die aus JUnit 3 bekannten Methoden des Software Testings nicht nur um die Verwendung von Annotationen, sondern bietet zusätzliche Mechanismen für die effektive Implementierung von Tests. So führt der Parameterized Testrunner einen Test mit einer Vielzahl von Parametern aus.

Ausgangspunkt

Der Artikel Testen mit JUnit gibt eine kurze Einführung in die Programmierung eines Tests mit JUnit 4. Als Beispiel einer Class under Test (CuT) dient die Berechnung der Fakultät. Der JUnit Tests führt eine Überprüfung der CuT durch Vorgabe einiger Eingangswerte und Vergleich des Berechnungsergebnisses mit dem erwarteten Wert durch. Der entsprechende Codeausschnitt sieht folgendermaßen aus:

  @Test
  public void testNfac() {
    assertEquals(1, calc.nfac(0));
    assertEquals(1, calc.nfac(1));
    assertEquals(2, calc.nfac(2));
    assertEquals(3628800, calc.nfac(10));
    assertEquals(2432902008176640000L, calc.nfac(20));
  }

Verbesserungspotenzial

Das erfüllt zwar seinen Zweck, ist aber für den Programmierer eine langweilige und fehlerträchtige Arbeit. Im Prinzip besteht die Testmethode doch aus vielen ähnlichen Zeilen, die sich nur in den Werten der Eingabe und Ausgabeparameter unterscheiden! Wäre es nicht besser, diese Parameter aus einer Liste zu laden und die Testmethode auf die Essenz des Tests, nämlich den Vergleich des Ergebnisses mit dem erwarteten Wert zu beschränken? Zum Beispiel also:

  @Test
  public void testNfac() {
    assertEquals(expected, calc.nfac(input));
  }

@RunWith

Die Lösung hierfür ist in JUnit 4 bereits enthalten und beginnt mit der Verwendung der Annotation @RunWith. Damit teilt man dem JUnit 4 Framework mit, dass es den Test mit dem spezifizierten Testrunner anstelle des eingebauten Runners ausführen soll. Für die angestrebte Parametrisierung des Tests eignet sich der in JUnit 4 eingebaute Testrunner Parameterized hervorragend:

package de.kompf.testing.basic;

import static org.junit.Assert.*;
import java.util.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

@RunWith(Parameterized.class)
public class ParameterizedCalculatorTest {
  
  private static Calculator calc = new Calculator();  

  private int input;
  private long expected;
  
  public ParameterizedCalculatorTest(int input, long expected) {
    this.input = input;
    this.expected = expected;
  }

Die Testklasse benötigt bei der Verwendung des Parameterized Runners einen speziellen Konstruktor, der zur Injektion der Testparameter durch den Testrunner dient. Dieser Konstruktor kopiert die Testparameter in Instanzvariablen, die damit für die oben dokumentierte Testmethode testNfac zur Verfügung stehen.

@Parameters

Zur Komplettierung des Tests fehlt nun nur noch die Definition der Parameterliste, mit denen der Test ablaufen soll. Dazu stellt der Programmierer eine statische Methode mit der Annotation @Parameters bereit:

  @Parameters
  public static List<Object[]> data() {
    return Arrays.asList(new Object[][] { { 0, 1 }, { 1, 1 }, { 2, 2 },
        { 10, 3628800 }, { 20, 2432902008176640000L } });
  }
}

Ergebnistyp ist eine Liste von Object Arrays. Anzahl und Reihenfolge der Elemente der Arrays entsprechen dabei den Konstruktorparametern, hier also input und expected.

Lässt man den Test jetzt ablaufen - in Eclipse mittels Run As - JUnit Test - dann generiert der Testrunner für jedes Wertearray aus der Liste einen Testfall und führt diesen aus. Im Beispiel führt dies zu fünf unterschiedlichen Tests.

Ihre wahre Kraft entfalten Parameterlisten, wenn man sie nicht im Quellcode hinterlegt, sondern während der Laufzeit dynamisch lädt - beispielsweise aus einer Textdatei oder einer Datenbank. Die folgende Methode lädt die Parameter für den Fakultätstest aus der Properties-Datei testparams.txt:

  @Parameters
  public static List<Object[]> data() throws IOException {
    try (Stream<String> lines = Files.lines(Paths.get("testparams.txt"))) {
      return lines.map(line -> line.split(":"))
          .map(s -> new Object[] { Integer.parseInt(s[0]), Long.parseLong(s[1]) } )
          .collect(Collectors.toList());
    }
  }

Die Properties-Datei enthält pro Zeile jeweils durch Doppelpunkt getrennte Eingabe- und Ergebnisparameter. Eine solche Datei - beispielsweise mit 20 Einträgen - kann man leicht unter Zuhilfenahme eines Perl Einzeilers erstellen:

perl -MMath::BigInt -le 'foreach (0..20) {print $_,":",Math::BigInt->new($_)->bfac()}' > testparams.txt

Fazit

Das Beispiel zeigt eindrucksvoll, wie sich unter Zuhilfenahme des in JUnit 4 eingebauten Parameterized Testrunners und cleveren Einsatzes von Tools wie Perl eine Vielzahl von Tests mit unterschiedlichen Parametern ausführen lassen. Dabei konnte man komplett auf fehlerträchtiges und langweiliges Copy&Paste von Testanweisungen verzichten. Mit etwas Fantasie ist die Vorgehensweise auf viele Testszenarien erweiterbar. Die erforderlichen Test- und Vergleichsdaten sind oftmals schon Bestandteil der zu testenden Anwendung.

Als besonderes Schmankerl kann man dann noch den Parameterized Testrunner durch den Parallelized Runner von Harald Wellmann ersetzen. Er führt die einzelnen Tests nicht sequentiell, sondern parallel aus. Neben einer Zeitersparniss bringt das dann auch Gewissheit, ob die CuT threadsafe programmiert ist.