Einleitung
Die Version 4 von JUnit brachte mit den Annotationen eine optisch sehr auffallende Neuerung. Diese trägt erheblich dazu bei, Tests besser zu überblicken und zu entwickeln. Weniger bekannt dagegen ist der neue Testrunner für sogenannte Theorien, der erst mit der Version 4.4 eingeführt wurde. Wie man Theorien einsetzt, will ich demonstrieren.
Theoretischer Hintergrund
Funktionen bzw. Methoden weisen Eigenschaften oder Regeln auf, die sich mit Hornformeln beschreiben lassen. Beispiele:
(sin(x) = y ) –> (sin(x+2pi) = y) (man könnte auch schreiben: wahr –> sin(x) = sin (x+2pi))
(cos(x) = y ) –> (cos(-x) = y)
aber auch:
(x > 0 ) –> (f(x) ist konvex)
(x < 0) und (y<0) –> (f(x,y) > 0)
Jede Funktion kennt irgendwelche Regeln. Es wäre möglich, mit den herkömmlichen Mitteln solche Regeln abzutesten, aber nicht sehr übersichtlich und effizient. Man müsste innerhalb einer Testmethode eine Schleife ablaufen lassen und noch einige Logik drumherum bauen.
Vorgehen
Die Datenbasis
Alle Theorien einer Testklasse verwenden diesselbe Datenmenge. Eine Datenmenge besteht aus sogenannten Datenpunkten, die der Tester @Datapoint deklariert:
@Datapoint
public static final int a = 1;
@Datapoint
public static final int b = 4;
@DataPoint
public static final Person beck = new Person("Kent", "Beck") |
@Datapoints
public static final double[] d_array = {0.2, 0.0, -0.5, 2.0};
Die Testmethoden für Theorien
Diese Testmethoden unterscheiden sich von herkömmlichen Testmethoden:
- Sie werden mit @Theory annotiert.
- Sie sind parametrisiert.
Der Testrunner analysiert die Signatur einer solchen Testmethode und ruft sie in jeder möglichen Kombination auf. Das hängt von den Typen der Parameter ab. Ein Beispiel:
@Theory
public void demoTheory(int a, int b) {
...
} |
Bei n Integer-Variablen in der Datenbasis, wird diese Methode n^2 mal aufgerufen.
@Theory
public void demoTheory2(int a, double c, double d) {...
} |
Wir haben wieder n Integer-Variablen und k Fließkommazahlen, dann gibt
es n*k² Kombinationen.
Aussortieren von Parameterkombinationen
Viele Eigenschaften von Testobjekten gelten nur, wenn bestimmte Vorbedingungen erfüllt sind. Die Methode bricht ab, wenn einem assumeTrue eine unwahre Aussage übergeben wird. Dies ist im JUnit-Kontext weder ein Failure noch ein Error. Geschieht dies, wird mit dem nächsten Testvektor weitergetestet.
Beispiel:
@Theory
public void demoTheory4(int x) {
assumeTrue(x>0);
assertTrue(f.isConvexAt(x));
} |
Komplexbeispiel
Jeder erfahrene Java Programmierer weiß, wie man die Methoden equals
und hashCode zu implementieren hat.
Für die equals-Methode müssen die Eigenschaften Reflexivität (1), Transitivität
(2) und Symmetrie (3) gelten:
wahr –> a.equals(a) (1)
a.equals(b) und b.equals(c) –> a.equals(c) (2)
a.equals(b) –> b.equals(a) (3)
Es muss außerdem gelten, dass gleiche Objekte (bzgl. der
equals-Methode) gleiche HashCodes erzeugen müssen:
a.equals(b) –> a.hashcode() == b.hashcode() (4)
Als Beispiel dient die Implementierung einer Klasse für Dreiecke. Zwei Dreiecke sollen als gleich gelten, wenn sie kongruent sind. Kongruenz besteht, wenn die beiden Dreiecke in ihren Seitenlängen übereinstimmen.
Die Testklasse
package theoryexample;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import org.junit.Test;
import org.junit.experimental.theories.DataPoint;
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;
@RunWith(Theories.class)
public class DreieckTest {
@DataPoint
public static final Dreieck d1 = new Dreieck(0.0, 0.0, 1.0, 1.0, 2.0, 2.0);
@DataPoint
public static final Dreieck d2 = new Dreieck(0.5, 0.5, 1.5, 1.5, 2.5, 2.5);
@DataPoint
public static final Dreieck d3 = new Dreieck(0.0, 0.0, 1.0, 1.0, 3.0, 3.0);
@DataPoint
public static final Dreieck d4 = new Dreieck(0.0, 0.0, -1.0, -2.0, -2.0, -2.0);
@DataPoint
public static final Dreieck d5 = new Dreieck(0.5, 0.5, 1.5, 1.5, -2.5, -2.5);
@DataPoint
public static final Dreieck d6 = new Dreieck(-1.0, 0.0, -2.0, -2.0, -3.0, -2.0);
@DataPoints
public static final Dreieck[] nochmehrDreiecke = {
new Dreieck(-1.0, -2.0, -2.0, -2.0, -3.0, 2.0),
new Dreieck(-1.0, -1.0, -2.0, -2.0, -4.0, 2.0),
new Dreieck(-1.0, 0.0, -2.0, -2.0, -5.0, 2.0),
new Dreieck(-1.0, 2.0, -2.0, -2.0, -7.0, 2.0)
};
@Theory
public void symmetrie(Dreieck a, Dreieck b) {
assumeTrue(a.equals(b));
assertTrue(b.equals(a));
}
@Theory
public void reflexivitaet(Dreieck a) {
// assumeTrue(true);
assertTrue(a.equals(a));
}
@Theory
public void transitivaet(Dreieck a, Dreieck b, Dreieck c) {
assumeTrue(a.equals(b));
assumeTrue(b.equals(c));
assertTrue(a.equals(c));
}
@Theory
public void equalsHashcode(Dreieck a, Dreieck b) {
assumeTrue(a.equals(b));
assertEquals(a.hashCode(), b.hashCode());
}
} |
Die Klasse Dreieck
package theoryexample;
public class Dreieck {
public static final double EPSILON = 0.00001;
public static boolean equalDoubles(double a, double b) {
double diff = a - b;
return (-EPSILON < diff ) && (diff < EPSILON);
}
private double[] coord_x = {0.0, 0.0, 0.0};
private double[] coord_y = {0.0, 0.0, 0.0};
public Dreieck(double x1, double y1, double x2, double y2, double x3, double y3) {
this.coord_x[0] = x1;
this.coord_x[1] = x2;
this.coord_x[2] = x3;
this.coord_y[0] = y1;
this.coord_y[1] = y2;
this.coord_y[2] = y3;
}
public boolean equals(Object other){ //Im Sinne von Kongruenz, d.h. Deckungsgleichheit
if (! (other instanceof Dreieck))
return false;
Dreieck other_cast = (Dreieck) other;
boolean[] checked = {false, false, false};
OUTER:
for (int i=0; i<3; i++) {
double a=lengthOfSide(i);
for (int j=0; j<3; j++) {
double b=other_cast.lengthOfSide(j);
if (!checked[j] && equalDoubles(a,b)) {
assert checked[j]==false;
checked[j]=true;
continue OUTER;
}
}
return false;
}
assert (checked[0]&&checked[1]&&checked[2]);
return true;
}
public int hashCode(){
int sum=0;
for (int i=0; i<3; i++) {
sum+=(int) lengthOfSide(i);
}
return sum;
}
public static double length(double a1, double b1, double a2, double b2) {
double da=a2-a1;
double db=b2-b1;
return Math.sqrt(da*da + db*db);
}
public double lengthOfSide(int n) {
if (! (n>=0 && n<=2)) {
throw new IllegalArgumentException();
}
return length(coord_x[n], coord_y[n], coord_x[(n+1)%3], coord_y[(n+1)%3]);
}
} |
Alexander Draeger