Über den Horizont

JUnit erweitern mit Rules und Extensions

Je umfangreicher die Testsuite wird, desto häufiger treten wahrscheinlich Klassen auf, die prinzipiell den gleichen Code für Setup und TearDown verwenden. Für diese Situation gab es mit JUnit 4 in der Regel drei Möglichkeiten: Auslagern in statische Klassen (Naja), abstrakte Oberklassen (Uh-Oh) oder … Copy & Paste (Kreisch!!!)

Don’t Repeat Yourself

Mit Junit 4.9 wurden die Test Rules eingeführt, die es ermöglichen, den Code für alle @Before/@After-Methoden in eigene Klassen auszulagern. Das erlaubte dann auch den Anbietern von Frameworks, ihren Code aus den Test-Runnern in solche Klassen auszulagern, auszuliefern und wiederverwendbar zu machen. Da mehrere Rules (für @Before/@After) und Classrules (für @BeforeClass/@AfterClass) verwendet werden können, lösten sich auch die Konflikte in der Verwendung von @RunsWith mit Suite, Enclosed, Categorized und Parameterized auf.

Rule

Eine Rule läßt sich einfach durch Implementierung von TestRule umsetzen. Ein Aufruf von evaluate() führt darin den Test aus:

public class CustomRule implements TestRule {
    @Override public Statement apply(Statement base, Description description) {
        return new Statement() {
            @Override public void evaluate() {
                …  // @Before/@BeforeClass
                try {
                    base.evaluate(); // @Test run
                } finally {
                    … // @After/@AfterClass
                } 
            }
        }
    }
}

Watcher

Übersichtlicher, und ohne innere Vererbungen, geht es mit einer Implementierung von TestWatcher:

public class CustomWatcher extends TestWatcher {
    @Override protected void starting(Description description) {
        … // @Before/@BeforeClass
    }

    @Override protected void finished(Description description) {
        … // @After/@AfterClass
    }
}

Extension (JUnit 5)

Für Junit 5 wurden sowohl TestRunner als auch Rules durch das Extension Model ersetzt. Zwar gibt es noch eine Unterstützung von auf TestWatcher basierenden Rules, für die folgende Abhängigkeit benötigt wird:

org.junit.jupiter:junit-jupiter-migrationsupport:5.3.1

Es empfiehlt sich jedoch, vorhandene Testklassen selbst nach Junit 5 zu migrieren, was nicht sehr aufwendig ist. Das erlaubt zudem eine detaillierte Strukturierung der Erweiterung.
Für jeden Schritt im Lebenszyklus eines Tests gibt es ein Interface mit einer zu überschreibenden Methode, die die entsprechenden Anweisungen enthält. So kann der Bedarf für jede Kombination von Test-Regeln gedeckt werden:

public final class CustomExtension implements
    TestInstancePostProcessor, TestExecutionExceptionHandler,
    BeforeAllCallback, BeforeEachCallBack, BeforeTestExecutionCallback,
    AfterTestExecutionCallback, AfterEachCallback, AfterAllCallback {

    @Override public void postProcessTestInstance(Object testInstance, ExtensionContext context) { … }

    @Override public void handleTestExecutionException(ExtensionContext context, Throwable throwable) { … }

    @Override public void beforeAll(ExtensionContext context) { … }
    @Override public void beforeEach(ExtensionContext context) { … }
    @Override public void beforeTestExecution(ExtensionContext context) { … }

    @Override public void afterTestExecution(ExtensionContext context) { … }
    @Override public void afterEach(ExtensionContext context) { … }
    @Override public void afterAll(ExtensionContext context) { … }
}

Wie man sieht, gibt es zusätzlich die Möglichkeiten, seinen Code

  • direkt vor oder nach dem Testmethoden-Aufruf auszuführen (d.h. jeweils zwischen @BeforeEach, @Test, und @AfterEach),
  • als erste Aktion nach der Instanziierung (postProcessTestInstance)
  • oder in Fehlerfällen (handleTestExecutionException).

Die Ausführung der Extensions, von denen es in einer Testklasse auch mehrere geben kann, erfolgt automatisch geschachtelt in der Reihenfolge der Angabe in der Annotation:

@ExtendWith({ First.class, Second.class, Third.class, … })