Mit ‘JUnit’ getaggte Artikel

Randomisierte parametrisierte Tests in JUnit

Montag, 04. Mai 2009

JUnit kennt neben dem Testrunner für einfache Testfälle auch noch den Testrunner für Theorien, den ich bereits vorgestellt habe, und den für parametrisierte Tests. Möchte man einen dieser beiden Testrunner verwenden, steht man unweigerlich vor dem Problem, welche Eingabedaten verwendet werden sollen. Insbesondere deren Erzeugung ist eine immer wiederkehrende und monotone Arbeit, die man sich sparen möchte.

Die in dem Artikel über  parametrisierte Tests vorgestellte Herangehensweise, ermöglicht es Zufallsdaten für parametrisierte Tests und Theorien zu liefern. Sie lässt auch viel Raum für weitere Ideen. Für gute Vorschläge und Denkanstöße wären wir dankbar.


Alexander Draeger


Organisation von Testklassenhierarchien

Freitag, 27. März 2009

Jeder weiß, dass frühzeitiges und intensives Testen die Entwicklungszeit und -kosten gering hält und trotzdem: Der Softwaretest ist ein gern vernachlässigtes Kapitel der Softwareentwicklung. Es mag vielleicht daran liegen, dass viele Unit-Tests zu viel sich wiederholenden Quelltext haben. Dabei bringen die objektorientierten Programmiersprachen Konzepte mit, die man in den Tests so einsetzen kann wie in dem getesteten System (SUT, system under test).

Die Konzepte Vererbung, Polymorphie und Generizität erlauben es, klassenorientierte Testfälle zu schreiben, die dann auf alle Unterklassen der CUT (class under test) angewandt werden können. In der Literatur gibt es dazu den Begriff des symmetrischen Testtreibers („Symmetric Driver“), was nichts anderes heißt, als dass parallel zu einer Klassenhierarchie eine Testklassenhierarchie geschrieben wird. Der Name resultiert daher, dass die Testklassenhierarchie symmetrisch zur Klassenhierarchie des SUT ist. Das beigefügte Dokumente zeigt, wie die Testfallhierarchie zu einer Klassenhierarchie aussehen kann. Strukturelemente wie die Testobjekte (OUTs, OUT = object under test) sollten protected sein, sodass Unterklassen darauf zugreifen können. Die Testmethoden sind public, wie es das Testframework, z. B. JUnit, erfordert.

Illustration der allgemeinen Testklassenhierarchie

Dass Testfälle auf Unterklassenmethoden wiederholt werden, klingt vielleicht nach Redundanz, ist aber pure Notwendigkeit. Die kürzlich mit dem Turing-Preis ausgezeichnete Barbara Liskov postulierte, dass Vorbedingungen (oder auch: der Definitionsbereich) einer überschriebenen Methode nicht eingeschränkt werden dürfen. Die Nachbedingungen dürfen eingeschränkt werden, aber nicht erweitert werden. Solche Eigenschaften sollten nach Möglichkeit durch die gesamte Klassenhierarchie erhalten bleiben und eignen sich hervorragend für der „Symmetric Driver“.

Der Aufbau der Testklassenhierarchie demonstriert die Stärke objektorientierter Modellierung. Objektorientierte Softwareentwicklung hat viele Fehlerquellen beseitigt, aber es kommen auch neue hinzu, wie z. B. eingeschränkte Vorbedingungen von überschriebenen Methoden. Die Testklassenhierarchie ist hilfreich beim systematischen Aufdecken von Fehlern des objektorientierten Entwurfes.

Die Verletzung des Liskovschen Substitutionsprinzips zeigt folgendes Beispiel:

class A {
  public void method(int n) {
     if (n < 0) throw new IllegalArgumentException();
  }
}
 
class B extends A {
 public void method(int n) {
     if (n < 1) throw new IllegalArgumentException();
  }

Die folgende Testklassenhierarchie findet den Fehler, vorausgesetzt, es werden die richtigen Datenpunkte definiert.

@RunWith(Theories.class)
class TestA {
 
   @Datapoint
   public static final Integer int1 = Integer.valueOf(0);
   protected A out;
  @Before
   public void setUp() {
     out = new A();
   }
 
   @Theory
   public smokeTest(int n) {
     assumeTrue(n>=0);
     out.method(n);
   }
}
 
class TestB extends TestA {
  @Before
   public void setUp() {
      out = new B();
   }
}

Alexander Draeger


“Theorien” in JUnit 4.4 – Anmerkung

Montag, 17. November 2008

Man kann Theorien sehr effektiv auf alle möglichen Klassen des System ausdehnen, ohne viel Zusatzarbeit. Die im Beispiel getesteten Eigenschaften bzgl. equals und hashcode müssen auch für alle Klassen gelten. Der Schlüssel hierzu ist eine generische, abstrakte Klasse, von der abgleitet wird.

package theoryexample;
 
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
 
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;
 
@RunWith(Theories.class)
public abstract class ObjectTest &lt;ClassUnderTest&gt; {
 
	@Theory
	public void symmetrie(ClassUnderTest a, ClassUnderTest b) {
		assumeTrue(a.equals(b));
		assertTrue(b.equals(a));
	}
 
	@Theory
	public void reflexivitaet(ClassUnderTest a) {
		// assumeTrue(true);
		assertTrue(a.equals(a));
 
	}
 
	@Theory
	public void transitivaet(ClassUnderTest a, ClassUnderTest b, ClassUnderTest c) {
		assumeTrue(a.equals(b));
		assumeTrue(b.equals(c));
		assertTrue(a.equals(c));
	}
 
	@Theory
	public void equalsHashcode(ClassUnderTest a, ClassUnderTest b) {
		assumeTrue(a.equals(b));
		assertEquals(a.hashCode(), b.hashCode());
	}
 
}
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;
 
public class DreieckTest extends ObjectTest  {
	@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)
	};
 
}

Alexander Draeger


TestRunner für “Theorien” in JUnit 4.4

Donnerstag, 13. November 2008

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:

  1. Sie werden mit @Theory annotiert.
  2. 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&gt;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 &lt; diff ) &amp;&amp; (diff &lt; 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&lt;3; i++) {
			double a=lengthOfSide(i);
			for (int j=0; j&lt;3; j++) {
				double b=other_cast.lengthOfSide(j);		
 
				if (!checked[j] &amp;&amp; equalDoubles(a,b)) {
					assert checked[j]==false;
					checked[j]=true;
					continue OUTER;
 
				}
			}
			return false;
		}
 
		assert (checked[0]&amp;&amp;checked[1]&amp;&amp;checked[2]);
		return true;
 
	}
 
	public int hashCode(){
		int sum=0;
		for (int i=0; i&lt;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&gt;=0 &amp;&amp; n&lt;=2)) {
 
			throw new IllegalArgumentException();
		}
 
		return length(coord_x[n], coord_y[n], coord_x[(n+1)%3], coord_y[(n+1)%3]);
	}
 
}

Alexander Draeger