Motivation
Häufig gibt es die Anforderung eine Methode in unterschiedlichen Status zu testen. Dabei sind Aufruf und die Testvorbereitung bis auf bis auf Erwartungs- und Eingangswerte immer identisch. Mit anderen Worten: Wenn wir auf herkömmliche Art und Weise testen, haben wir einen relativ großen Aufwand an Codeduplication und Boilerplates, die bekanntlicher weise ein hohes Potential an Fehleranfälligkeit hat.
Warum also die Arbeit für jeden Statuswechsel, für alle Extremwert-Tests etc. neu programmieren? Warum kann ich nicht einen Test schreiben und den für alle Testvarianten nutzen? Schließlich ist der Aufruf der zu testenden Methode auch im Code später immer der gleiche – nur Daten uns Status variieren.
Genau das ist möglich mit parameterisierten Tests. Wie der Name bereits aussagt, werden hier Parameter, also Rahmenbedingungen, für Tests definiert, so dass die zu testende Methode, immer wieder unter anderen Bedingungen aufgerufen werden kann.
Aufbau der Tests
- Schritt:
Ein parametrisierter Test muss mit der Annotation@RunWith(Parameterized.class)beginnen. Somit ist eine Testklasse als parametrisierte Klasse definiert. - Schritt:
Die Testklasse benötigt eine Methode, die sowohl statisch als auch public ist und die Annotation@Parametersträgt. Auf diese Annotation werde ich später nochmal eingehen. Für den Augenblick reicht uns diese Annotation, um die parametrisierten Tests zu starten. Somit bekommen wir eine Methodepublic static Collection<Object[]> prepareTests() {...}, die unsere Testparameter vorbereitet. - Schritt:
In einfachen JUnit Tests reicht uns der Default-Constructor aus. Nicht so bei parametrisierten Tests. Hier wird ein Constructor benötigt, der die einzelnen Elemente des Arrays eines Elements der Collection enthält. Weiter unten im Beispiel sollte es deutlich werden, was ich meine. - Schritt:
Implementierung des eigentlichen Tests
Beispiel
Ich habe mir ein relativ einfaches Beispiel überlegt, bei dem es darum geht die Math-Klasse von Java zu testen.
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import java.util.ArrayList;
import java.util.Collection;
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 MathTest {
@Parameters(name = "{0} / {1}")
public static Collection<Double[]> prepareTests() {
final Collection<Double[]> testCases = new ArrayList<>();
for (int a = 0; a <= 10; a++) {
for (int b = 0; b <= 10; b++) {
final Double[] testCase = { (double) a, (double) b, (double) (a > b ? a : b),
(double) (a < b ? a : b), manuallPower(a, b) };
testCases.add(testCase);
}
}
return testCases;
}
private static double manuallPower(final double left, final double right) {
double result = 1;
for (int i = 0; i < right; i++) {
result *= left;
}
return result;
}
private final double left;
private final double right;
private final double powerResult;
private final double maxResult;
private final double minResult;
public MathTest(final double left, final double right, final double maxResult, final double minResult, final double powerResult) {
super();
this.left = left;
this.right = right;
this.maxResult = maxResult;
this.minResult = minResult;
this.powerResult = powerResult;
}
@Test
public void power() {
assertThat(Math.pow(left, right), is(powerResult));
}
@Test
public void max() {
assertThat(Math.max(left, right), is(maxResult));
}
@Test
public void min() {
assertThat(Math.min(left, right), is(minResult));
}
}
Mit der Methode „prepareTests“ werden die Tests vorbereitet. Ich habe hier auf das Generieren der Testdaten genommen. Natürlich kann man auch Testdaten aus einer Datenbank oder aus Dateien beziehen. Für die Generierung der Daten, wird eine Methode benötigt, die mir die Potenz meiner Testdaten berechnet. (Ist notwendig, weil die Daten generiert werden. Bei festen Daten kann man den Erwartungswert fest implementieren.)
Wie finden außerdem einen Konstruktor mit mehreren Parametern. Die Anzahl der Parameter entspricht der Anzahl der Daten in einem Array. Diese Daten werden nun in Member für den Test geschrieben, so dass diese in den einzelnen Test-Methoden verfügbar sind.
Zum Schluss sind noch die Test-Methoden zu finden, deren Aufbau uns ja aus nicht parametrisierten Test bekannt ist.
Unterschied zu @BeforeClass
Ähnlich wie mit @BeforeClass findet eine statische Testvorbereitung statt. Allerdings sorgt @BeforeClass nicht dafür, dass die Testklasse mehrfach instantiiert und ausgeführt wird. Bei @BeforeClass werden fixe Einstellungen, die für alle Tests gelten vorgenommen, wie z.B. Datenbankverbindungen, Pfadeinstellungen zum Laden von Dateien, o.ä.
Die parametrisierten Tests dagegen definiern in einem statischen Rahmen die Testdaten, die dann einzeln in den Test-Methoden genutzt und verarbeitet werden.