Advanced Java Services | Parameterized Test |
Methoden haben meistens Eingangsparameter und liefern dazu Ergebnisse. Fehler treten oft nur für bestimmte Eingangsparameter auf, für andere dagegen nicht. Es ist deshalb sinnvoll einen Test mit verschiedenen Parametern zu wiederholen. Um Tests mit verschiedenen Parametern zu wiederholen müssen wir die generierte Testklasse von Hand ergänzen. Dazu sind mehrere Schritte notwendig.
Wir gehen von einer sehr einfachen Klasse Datum aus. Sie enthält eine statische Methode, die wir testen wollen.
public class Datum { public static boolean isLeapYear(int year) { return (year%4 ==0 && year%100 != 0) || year%400 == 0; } }
Wir erzeugen mit JUNit eine Testklasse.
import org.junit.Test; public class DatumTest { @Test public void testIsLeapYear() { } }
Diese Annotation muß vor der Testklasse stehen. Sie gibt JUnit bekannt, daß wir eine Methode mit einer gewissen Anzahl von Werten wiederholt ausführen lassen wollen.
@RunWith(Parameterized.class)
Die Annotation braucht zwei Importe, der zweite Import wird von Eclipse nicht entdeckt und muß von Hand eingefügt werden.
import org.junit.runner.RunWith; import org.junit.runners.Parameterized;
Da wir nur eine Testmethode haben ist die Situation einfach. Wir wollen der Methode isLeapYear(int j) für verschiedene Jahreszahlen aufrufen und setzen eine zunächst noch undefinierte Variable ein.
@Test public void testIsLeapYear() { boolean boo = Datum.isLeapYear(year); Assert.assertTrue(year + " ist ein Schaltjahr : " + boo, boo); }
Die undefinierte Variable wird jetzt im Datenteil der Testklasse vereinbart. Sie erhält ihren Wert über einen passenden Konstruktor der Testklasse. Beim Instanziieren des Testobjekts kann damit die Variable initialisiert werden. JUnit legt ja für jede zu testende Methode ein eigenes Objekt an. Auf diese Weise gelangt der Parameter in die Testmethode.
import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @RunWith(Parameterized.class) public class DatumTest { private int year; public DatumTest(int year) { this.year = year; } @Test public void testIsLeapYear() { boolean boo = Datum.isLeapYear(year); Assert.assertTrue(year + "ist ein Schaltjahr", boo); } }
Nun legen wir noch eine statische Methode an. Mit ihr wird der Konstruktor der Testklasse mit konkreten Daten versorgt.
Der Name der statischen Methode kann theoretisch frei gewählt werden, man folgt aber der Konvention. Der Returntyp ist festgelegt und muß vom Typ Collection<Object[]> sein. Die Methode muß zudem die Annotation @Parameters erhalten, außerdem darf sie nicht null zurückgeben.
Für unseren Fall kann die Methode z. Bsp. so aussehen:
@Parameters public static Collection<Object[]> getParams() { Object[][] data = new Object[][] { {1800} , {1900}, {2000} , {2014}, {2015} , {2016} }; return Arrays.asList(data); }
Hier ist wichtig: data muß ein zweidimensionales Array vom Typ Object sein, auch wenn man in diesem Fall ein eindimesionales Arrays verwenden könnte!
import java.util.Arrays; import java.util.Collection; import org.junit.Assert; 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 DatumTest { private int year; public DatumTest(int year) { this.year = year; } @Test public void testIsLeapYear() { boolean boo = Datum.isLeapYear(year); Assert.assertTrue(year + "ist ein Schaltjahr", boo); } @Parameters public static Collection<Object[]> getParams() { Object[][] data = new Object[][] { {1600}, {1800}, {2000} , {2010}, {2015} , {2016} }; return Arrays.asList(data); } }
Die Testfolge wird wie ein einzelner Test gestartet. JUnit erkennt an Hand der Annotations, daß der Test mit den durch die statische Methode gelieferten Daten wiederholt werden soll. In diesem Fall gibt es also sechs Durchläufe.
Es ist nicht möglich mehrere Konstruktoren zu schreiben und die übergebenen Daten verschiedenen Testmethoden zuzuordnen. Will man mehrere Testmethoden wiederholen muß man alle Parameter über einen Konstruktor übergeben.
Wie wollen die zwei Methoden de folgenden Klasse testen:
public class Datum { private int tag, monat, jahr; public static boolean isLeapYear(int year) { return (year%4 == 0 && year%100 != 0) || year%400 == 0; } public Datum(int tag, int monat, int jahr) { this.tag = tag; this.monat = monat; this.jahr = jahr; } /** * Liefert den Wochentag als String */ public String getWochentag() { int wochentag; int h = monat, k = jahr ; if (monat < 3) { h = monat + 12 ; k = jahr-1 ; } //wochentag = (tag+2*h + (3*h+3)/5 + k + k/4 - k/100 + k/400 + 1)%7 ; // richtige Formel // Fehler in der Formel wochentag = (tag+2*h + (3*h+3)/5 + k + k/4 + k/100 - k/400 + 1)%7 ; switch (wochentag) { case 0 : return "Sonntag"; case 1 : return "Montag"; case 2 : return "Dienstag"; case 3 : return "Mittwoch"; case 4 : return "Donnerstag"; case 5 : return "Freitag"; case 6 : return "Samstag"; default: return "gibt's nicht"; } } } // end class
Dazu erzeugt uns JUnit folgende Testklasse.
import static org.junit.Assert.*; import org.junit.Test; public class DatumTest { @Test public void testIsLeapYear() { fail("Not yet implemented"); // TODO } @Test public void testGetWochentag() { fail("Not yet implemented"); // TODO } }
Die wir wie folgt ergänzen
import org.junit.Test; import org.junit.Assert; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @RunWith(Parameterized.class) public class DatumTest { private int year; private int tag, monat, jahr; private String wochentag; @Test public void testIsLeapYear() { boolean boo = Datum.isLeapYear(year); Assert.assertTrue(year + " ist ein Schaltjahr: " + boo, boo); } @Test public void testGetWochentag() { Datum date = new Datum(tag, monat, jahr); String wtag = date.getWochentag(); Assert.assertEquals(wochentag, wtag); } }
Jetzt brauchen wir noch einen Konstruktor und die statische Methode getParams()
import org.junit.Test; import java.util.Arrays; import java.util.Collection; import org.junit.Assert; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; @RunWith(Parameterized.class) public class DatumTest { private int year; private int tag, monat, jahr; private String wochentag; public DatumTest(int tag, int monat, int jahr, String wochentag, int year) { this.tag = tag; this.monat = monat; this.jahr = jahr; this.wochentag = wochentag; this.year = year; } @Parameters public static Collection<Object[]> getParams() { Object[][] data = new Object[][] { { 1, 1, 2016, "Freitag", 1600}, { 1, 2, 2016, "Montag", 1800}, { 1, 3, 2016, "Dienstag", 2000} , { 1, 4, 2016, "Freitag", 2010}, { 1, 5, 2016, "Sonntag", 2016} }; return Arrays.asList(data); } @Test public void testIsLeapYear() { boolean boo = Datum.isLeapYear(year); Assert.assertTrue(year + " ist ein Schaltjahr: " + boo, boo); } @Test public void testGetWochentag() { Datum date = new Datum(tag, monat, jahr); String wtag = date.getWochentag(); Assert.assertEquals(wochentag, wtag); } }
Durch geschickte Wahl der statischen Methode kann man beliebige Wiederholungen ohne Parameterübergabe erreichen. Man kann manchmal auch die Daten in der Testklasse direkt erzeugen. Mit der folgenden Testklasse kann man z. Bsp. die Methode isLeapYear(year) ebenfalls testen.
import org.junit.Test; import java.util.Arrays; import java.util.Collection; import org.junit.Assert; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; @RunWith(Parameterized.class) public class DatumTest { private static int year = 2009; public DatumTest() { ++year; } @Parameters public static Collection<Object[]> generateParams() { return Arrays.asList(new Object[7][0]); } @Test public void testIsLeapYear() { boolean boo = Datum.isLeapYear(year); Assert.assertTrue(year + " ist ein Schaltjahr: " + boo, boo); } }
Der Trick ist hier, die Variable statisch anzulegen. Da JUnit für jeden Durchlauf ein neues Objekt anlegt kann man nur so die Variable hochzählen.