JUnit erweitern

Aus Eclipse
Wechseln zu: Navigation, Suche

Erweiterungsmöglichkeiten

Da JUnit sich als Framework versteht, stellt sich schnell die Frage, welche Erweiterungsmöglichkeiten abseits der reinen Erstellung von Testklassen und Nutzung der Umgebung es bietet. Diese Möglichkeiten sollen in diesem Artikel diskutiert werden.

Da JUnit in Java geschrieben ist, bietet sich zunächst einmal die klassische Java-Möglichkeit der Erweiterung durch Ableitung an. Deshalb konnte man in JUnit 3 schon das Schreiben von Testfällen als Erweiterung von JUnit betrachten, schließlich wurde immer von der Klasse TestCase abgeleitet.

Seit JUnit 4 haben Testklassen nicht mehr notwendigerweise eine gemeinsame Ursprungsklasse, deshalb fällt diese Betrachtungsweise weg. Tests werden nun annotiert und nicht mehr abgeleitet. Nicht desto trotz ist das Framework natürlich weiterhin erweiterbar.

In diesem Artikel soll eine Erweiterung entwickelt werden, die JUnit 4 um eine Annotation @RunIf erweitert, um Tests oder Testklassen nur unter bestimmten Bedingungen auszuführen. Als Parameter soll dieser Annotation eine Klasse übergeben werden, die prüft, ob die Bedingung erfüllt ist.

Als mögliche Anwendungsfälle dieser Erweiterung sind Situationen zu nennen, in denen Tests nur auf bestimmten Betriebssystemen sinnvoll sind, oder zu bestimmten Tageszeiten, oder bei Konfigurationen mit einem bestimmten Datenbanksystem, oder wenn der Test mit einem privilegierten Account ausgeführt wird, usw. Außerdem ist die Erweiterung neben ihrer direkten Nutzbarkeit als Proof of Concept zu verstehen, sie zeigt, wie einfach JUnit erweitert werden kann.

RunIf

Die hier vorgestellte Möglichkeit, JUnit durch eigene Annotationen zu erweitern, basiert auf der Architektur von JUnit, Tests nicht direkt aus dem Framework heraus auszuführen, sondern dazu einen Runner zu benutzen. Mitgelieferte Runner finden sich im Package org.junit.runners. Wenn nichts Abweichendes angegeben ist, werden Tests immer mit dem Runner BlockJUnit4ClassRunner gestartet, doch Testklassen können angeben, dass sie von einem anderen Runner ausgeführt werden sollen. Von dieser Möglichkeit haben wir schon in den Artikeln Einführung in JUnit und JUnit in Eclipse Gebrauch gemacht, und zwar bei dem Zusammenfassen mehrerer Tests als Suite und beim Erstellen von Tests mit Parametern. In beiden Fällen gibt die Testklasse mithilfe der Annotation @RunWith an, einen anderen Runner benutzen zu wollen.

In diesem Artikel wird nun ein eigener Runner ExtClassRunner vorgestellt, der zusätzlich zum BlockJUnit4ClassRunner die Annotation @RunIf auswerten kann. Zunächst einmal hat das Interface RunIf, das die Annotation definiert, einen recht einfachen Aufbau.

 @java.lang.annotation.Retention(value = RUNTIME)
 @java.lang.annotation.Target(value = {METHOD, TYPE})
 public @interface RunIf {
     Class<? extends Check> wert(); 
 
     String[] argumente() default {};
 }

Es definiert lediglich, dass der Annotation als Parameter eine Klasse übergeben wird, die vom Interface Check abgeleitet wird, sowie ein Array von Strings als Argumente. Falls der Parameter argumente nicht angegeben ist, wird eine leere Liste übergeben. Die Parameterklasse beinhaltet dann die eigentliche Funktion, die überprüft werden soll vor Ausführung des Tests. Die so definierte Annotation kann sowohl Methoden als auch Klassen hinzugefügt werden.

Das Interface Check selber ist ebenfalls recht einfach:

 public interface Check {
     boolean erfuellt();
 }

Es wird lediglich eine Methode gefordert, die als Rückgabewert liefert, ob die Bedingung, die diese Klasse überprüfen soll, wahr ist.

Die eigentliche zu implementierende Logik liegt dann in der Klasse ExtClassRunner. Bei ihr ist zu beachten, dass die Annotation @RunIf an zwei unterschiedlichen Stellen stehen können soll. Zum einen sollen einzelne Testmethoden so gekennzeichnet werden können, zum anderen auch ganze Testklassen. Für den zweiten Fall wird die erste Überprüfung schon im Konstruktor der Klasse ExtClassRunner durchgeführt:

 public ExtClassRunner(Class<?> klasse) throws InitializationError {
 	super(klasse);
 	testFuerKlasseAusfuehren = istTestErfuelltClass(klasse);
 }

Das Ergebnis der Methode istTestErfuelltClass, die später vorgestellt wird, wird in einer boolschen Variable abgelegt. Nun ruft das Framework für jede Testmethode der Testklasse die Methode runChild des Runners auf, die ebenfalls überschrieben wird.

 @Override
 protected void runChild(FrameworkMethod method, RunNotifier notifier) {
    	if (testFuerKlasseAusfuehren && istTestErfuellt(method)) {
    		super.runChild(method, notifier);
    	} else {
            Description testBeschreibung = Description.createTestDescription(this.getTestClass().getJavaClass(),
                    method.getName());
            notifier.fireTestIgnored(testBeschreibung);
        }
 }

Hier wird zunächst überprüft, ob die ganze Testklasse nicht ausgeführt werden soll (testFuerKlasseAusfuehren). Falls doch wird anschließend überprüft, ob die Bedingung für diesen spezifischen Test erfüllt ist durch Aufruf der Methode istTestErfuellt, die ebenfalls noch vorgestellt wird.

Falls beides wahr ist, wird die Super-Methode aufgerufen, die den Test entsprechend auswertet, ansonsten wird der Test als ignoriert markiert.

Die beiden Methoden istTestErfuellt und istTestErfuelltClass unterscheiden sich nur in ihrem Parameter. Erstere nimmt die auszuführende Testmethode, letztere die ganze Testklasse. Beide erfragen vom übergebenen Objekt, ob eine Annotation RunIf angegeben ist. Wenn ja wird der hierin übergebene Test referenziert durch die Variable test. Beide Varianten von istTestErfuellt bedienen sich nun der privaten Methode starteTest, um den eigentlichen Test instanziieren zu lassen.

Die Methode starteTest unterscheidet hierzu, ob dem Check Argumente übergeben werden sollen. Falls nein, wird der Defaultconstructor aufgerufen. Falls ja, muss zunächst der passende Constructor ermittelt werden, dem dann eben diese Argumente übergeben werden. Die Methode liefert den instanziierte Check zurück.

Die beiden Methoden istTestErfuellt und istTestErfuelltClass können nun ermitteln, ob der Check positiv ist, und ebenfalls dieses Ergebnis liefern.

 public boolean istTestErfuellt(FrameworkMethod method) {
     RunIf resource = method.getAnnotation(RunIf.class);
     if (resource == null) {
         return true;
     }
     Class<? extends Check> test = resource.wert();
     try {
         Check checker = starteTest(resource, test);
         return checker.erfuellt();
     } catch (Exception e) {
         throw new RuntimeException(e);
     }
 }
 
 public boolean istTestErfuelltClass(Class<?> klasse) {
     RunIf resource = klasse.getAnnotation(RunIf.class);
     if (resource == null) {
         return true;
 
     }
     Class<? extends Check> test = resource.wert();
     try {
         Check checker = starteTest(resource, test);
         return checker.erfuellt();
     } catch (Exception e) {
         throw new RuntimeException(e);
     }
 }
 
 private Check starteTest(RunIf resource, Class<? extends Check> prerequisiteChecker) throws Exception {
     String[] arguments = resource.argumente();
     Check checker;
     if (arguments == null || arguments.length == 0) {
         checker = prerequisiteChecker.newInstance();
     } else {
         if (arguments.length == 1) {
             Constructor<? extends Check> constructor = prerequisiteChecker.getConstructor(String.class);
             checker = constructor.newInstance(arguments[0]);
         } else {
             Constructor<? extends Check> constructor = prerequisiteChecker.getConstructor(String[].class);
             checker = constructor.newInstance(new Object[]{arguments});
         }
     }
     return checker;
 }

Mit diesen Klassen ist es nun möglich, JUnit um eigene Bedingungen zu erweitern, wann bestimmte Tests ausgeführt werden sollen.

Checks erstellen

Die Klassen mit den Bedingungen müssen, wie im Vorkapitel schon beschrieben, das Interface Check implementieren. Hierbei ist lediglich die Methode erfuellt() zu implementieren. Diese liefert, ob die zu prüfende Bedingung wahr ist oder nicht. Welche Bedingungen hier geprüft werden und wie diese implementiert werden, liegt völlig in der Hand des Entwicklers.

Beispiel-Checker Betriebssystem

Ein Beispiel könnte eine Klasse sein, die überprüft, ob der Test auf einem bestimmten Betriebssystem ausgeführt wird. Dies kann beispielsweise wie folgt implementiert werden:

 public class Betriebssystem implements Check {
    public static final String MAC = "mac";
    public static final String LINUX = "linux";
    public static final String WINDOWS = "win";
 
    private final String os;
 
    public Betriebssystem(String os) {
        this.os = os;
    }
 
    public boolean erfuellt() {
        String osName = System.getProperty("os.name");
        return osName.toLowerCase().contains(os);
    }
 }

Als Parameter wird der Bedingung das Betriebssystem übergeben, auf dem der Test ausgeführt werden darf. Die Klasse überprüft nun in der Methode erfuellt(), ob in der entsprechenden Java-Umgebungsvariable os.name das Betriebssystem enthalten ist. Somit ist sichergestellt, dass entsprechend markierte Tests nur ausgeführt werden, wenn das umgebende Betriebssystem dies sinnvoll erscheinen lässt.

Beispiel-Checker File

Eine weitere Idee, die mit Hilfe eines solchen Checkers realisiert werden kann, ist die Überprüfung, ob eine bestimmte Datei vorhanden ist. Ein Checker hierzu sieht wie folgt aus:

 public class FileExists implements Check {
 
 	private String pfad;
 
 	public FileExists(String pfad) {
 		this.pfad = pfad;
 	}
 
 	@Override
 	public boolean erfuellt() {
         if (pfad == null || pfad.trim().equals("")) {
             throw new RuntimeException("Bitte Pfad angeben");
        }
        return new File(pfad).exists();
	}
 }

Ein Beispiel

Ein Beispiel für die Verwendung eines solchen Checker wäre folgendes:

@RunWith(ExtClassRunner.class)
public class CalculatorTest {
 
	private static Calculator calculator;
 
	@BeforeClass
	public static void switchOnCalculator() {
		System.out.println("\tSwitch on calculator");
		calculator = new Calculator();
		calculator.switchOn();
	}
 
 
	@AfterClass
	public static void switchOffCalculator() {
		System.out.println("\tSwitch off calculator");
		calculator.switchOff();
		calculator = null;
	}
 
	@Before
	public void clearCalculator() {
		calculator.clear();
	}
 
	@Test
	@RunIf(wert = Betriebssystem.class, argumente = Betriebssystem.MAC)
	public void add() {
		calculator.add(1); 
		calculator.add(1); 
		assertEquals(calculator.getResult(), 2); 
	}
}

Beispielcode

Der komplette Code zu diesem Artikel findet sich hier: JUnitErweiternBeispiel.zip


Nächster Artikel: JUnit TestViewer Plugin