Autorenarchiv

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


Themes, Tags und Templates in Struts2

Montag, 13. Oktober 2008

In Struts2 ist die Art und Weise, mit der UI-Tags gerendert werden, in so genannten Themes zusammengefaßt. Diese Themes bestehen aus einzelnen FreeMarker-Templates, wobei für jeden Tag mehr oder weniger ein Template existiert.

In Struts2 sind mehre Themes schon von Hause aus enthalten:

  • simple
  • xhtml
  • css_xhtml
  • ajax

Das “simple”-Theme stellt dabei nur die Basisfunktionalität von Struts2 zu Verfügung und überläßt das Design vollständig dem Entwickler. Alle anderen Themes stellen u.A. clientseitige, JS-basierte Validierung zur Verfügung, greifen allerdings auch in Designentscheidungen ein. Wobei das “css-xhtml”-Theme ein zweispaltiges Layout wählt und alle Elemente in <div>-Tags kapselt, so dass diese mit CSS formatiert werden können. Das “xhtml”-Theme und das darauf aufbauende “ajax”-Theme bestimmen von sich aus viele Teile des Designes. So statten sie input-Felder gleich mit einer Beschreibung aus und platzieren Feldfehler direkt an das auslösende Feld. Da man aber meist ein anderes Design wünscht, als die Struts2-Entwickler sich denken, kann man eigene Themes erstellen oder bestehende Themes anpassen.

Als Grundlage bietet sich das “simple”-Theme an, da es den Entwickler am wenigsten “bevormundet”. Um nicht das Rad komplett neu zu erfinden, kann man das “simple”-Theme an das eigene Theme vererben. Dies geschieht durch den Eintrag parent=simple in der “theme.properties”-Datei im Verzeichnis des neuen Themes, also z.B. “/template/buj_theme/theme.properties”. Ist nun das “buj_theme” ausgewählt, werden die Templates in diesem Ordner benutzt, falls vorhanden, sonst das “simple”-Theme.

Welches Theme benutzt wird, kann von Tag zu Tag entschieden werden (Tag-Attribut theme=“…”) und es kann ein globales Default-Theme gewählt werden, beispielsweise durch struts.ui.theme=buj_theme in der “struts.properties”.

Möchte man eigene Tags erstellen, so braucht man nur eine neue FreeMarkerDatei (.ftl-Endung) in dem Template-Verzeichnis erstellen, auf die man dann mit

<s:component template="meinTag"/>

zurückgreifen kann. Mit

<s:component template="meinTag">
    <s:param name="param1" value="value1"/>
</s:component>

kann man Parameter an die Tagauswertung übergeben, die dann in dem Template unter Anderem mit ${parameters.param1} ausgelesen werden können.

In den Freemarker-Templates kann man auch die aus Struts2 bekannte Sprache OGNL benutzen, allerdings mit $ anstatt %. Beachte: ${parameters.x} greift hierbei auf die Paramter/Attribute das Tags zu während ${Parameters.x} den Requestparameter x ausliest.

Probleme: Modifiziert man bestehende Templates, so kann es vorkommen, dass Tag-Attribute die Action-Properties überschatten. Gibt es in der Action ein Feld “id” so kann nicht (OGNL typisch) mit ${id} darauf zugegriffen werden, wenn das Tag auch ein Attribut “id” besitzt, selbst wenn dieses nicht gesetzt ist.

Auf das Action-”id” kann statt dessen mit ${action.id} zugegriffen werden.

Möchte man in dem Template Struts2-Tags benutzen, kann man dies mit <@s.iterator… statt mit <s:iterator… tun.

Leider ist das Thema “Schreiben eigener Tags” in FreeMarker/Struts2 kaum dokumentiert, vor allem was die Struts2 spezifischen Details angeht, und deshalb heißt es im Moment leider noch „Probieren geht über Studieren“.


Alexander Draeger