© Excellent backgrounds/Shutterstock.com
Java-8-Features in JUnit-Tests

Tests besser wiederverwenden


Dieser Artikel ist kein Plädoyer für das allumfassende Testen von Softwarekomponenten; er ist auch keine Aufforderung zur Verwendung allein testgetriebener Vorgehensmodelle. Vielmehr stellt dieser Artikel mit J8Unit einen pragmatischen Ansatz für eine deutlich verbesserte Wiederverwendbarkeit von Testcode vor, der sowohl an Tester als auch an API-Designer gerichtet ist.

Erfahrungsgemäß lässt sich kaum ein Softwareentwickler finden, der nicht mindestens per Lippenbekenntnis die Relevanz von Softwaretests, insbesondere automatisierter Unit Tests, bestätigen würde. Für Drittbibliotheken gilt jedoch, dass – neben der Annahme, diese seien zumindest funktional hinreichend getestet – selten Testcode existiert oder bereitgestellt wird. Einer der Gründe für diese Situation ist die oft unterschiedliche Herangehensweise an die Programmierung von Code und Testcode. Die eigentliche Software soll häufig den üblichen Qualitätskriterien genügen (z. B. ISO/IEC 25000). Testcode hingegen wird eher begleitend erstellt und ohne wesentliche Eigenschaften zu beachten – insbesondere die Wiederverwendbarkeit. Wenn überhaupt, wird Testcode eher aus dokumentarischen Gründen bereitgestellt als mit der Intention, diesen nachnutzbar zu gestalten. Dies zwingt den Nutzer eines API dazu, gleichartige, unter Umständen duplizierte Tests für die eigenen Komponenten nach seinem API-Verständnis zu implementieren.

Doch selbst wenn nachnutzbarer Testcode bereitsteht, gestaltet es sich problematisch, diesen wiederzuverwenden, da bei derartigen Methoden (Listing 1) viel unnötiger Boilerplate-Code entstehen muss, um die Testmethoden jeweils mit dem passenden Testsubjekt (Subject Under Test, SUT) aufzurufen (Listing 2). Alternativ werden nachnutzbare Testmethoden über entsprechende Testklassenhierarchien bereitgestellt (Listing 3). Das schränkt allerdings die Freiheit des Testers ein, da er einer vorgegebenen Vererbungslinie folgen muss (Listing 4). Beide Varianten sind daher nur bedingt praktikabel.

Listing 1

import org.junit.*; public class ReusableTests { public static void testFoobar1(Foobar sut) {  // ... Assert.assertXXX(...); } public static void testFoobar2(Foobar sut) {  // ... Assert.assertXXX(...); } }

Listing 2

import org.junit.*; public class OldStyleTest1 { @Test public void testFoobar1() { Foobar sut = new Foobar(); ReusableTests.testFoobar1(sut); } @Test public void testFoobar2() { Foobar sut = new Foobar(); ReusableTests.testFoobar2(sut); } }

Listing 3

import org.junit.*; public abstract class ExtendableTests { protected abstract Foobar createNewSUT(); @Test public void testFoobar1() { Foobar sut = createNewSUT();  // ... Assert.assertXXX(...); } @Test public void testFoobar2() { Foobar sut = createNewSUT();  // ... Assert.assertXXX(...); } }

Listing 4

public class OldStyleTest2 extends ExtendableTests { @Override protected Foobar createNewSUT() { return new Foobar(); } }

Die in Java 8 erschienenen Sprachfeatures der default-Methoden in Interfaces [1], [2] können die Einschränkung vermeiden, wenn anstatt eine Oberklasse restriktiv zu verwenden, beliebig nachnutzbare Interfaces eingesetzt werden. Natürlich muss das jeweils zum Einsatz kommende Testframework dieses neue Sprachfeature auch unterstützen.

J8Unit-Testklassenmodell erweitert

Im Zusammenhang mit JUnit bedeutet dies, dass das Testklassenmodell um die Erkennung von @Test‑annotierten default-Interfacemethoden erweitert werden muss. Ein entsprechend erweitertes Testklassenmodell ist daher wesentlicher Bestandteil von J8Unit. Zudem müssen die bei Testausführung genutzten Runner-Klassen dieses erweiterte Modell verwenden; entsprechende Runner-Klassen sind ebenfalls in J8Unit enthalten. Die Definition zusätzlicher Runner ist notwendig, da JUnit keine Möglichkeit bereitstellt, das zu verwendende Testklassenmodell in bestehende Runner hineinzureichen.

J8Unit für Tester

Wie bei jeder Fremdbibliothek üblich, muss auch J8Unit zur Verwendung im Java-Classpath vorhanden sein. Für Maven ist eine entsprechende Dependency (mit Test-Scope) zu ergänzen:

<dependency> <groupId>org.j8unit</groupId> <artifactId>core</artifactId> <version>4.12</version> <scope>test</scope> </dependency>

Dadurch wird es möglich, @Test-annotierte default-Methoden aus beliebigen Interfaces (Listing 5) zu integrieren. Listing 6 zeigt ein Beispiel unter Verwendung des erweiterten Default-Runners.

Listing 5

import org.junit.*; public interface ITest1 { @Test public default void testFoo() { Foobar sut = new Foobar();  // ... Assert.assertXXX(...); } @Test public default void testBar() { Foobar sut = new Foobar();  // ... Assert.assertXXX(...); } }

Listing 6

import org.j8unit.runners.J8Unit4; import org.junit.*; import org.junit.runner.*; @RunWith(J8Unit4.class) public class Test1 implements ITest1 { @Test // additional test method public void testFoobar() { Foobar sut = new Foobar();  // ... Assert.assertXXX(...); } }

Es wird jedoch schnell offensichtlich, dass sich per Interface eingebundene Tests zwar wiederholen lassen, diese Testwiederholungen aber nur selten Mehrwert bringen, wenn sie stets auf demselben SUT basieren. Sinnvolle Ausnahme sind etwa wiederverwendbare Begleittests, um Annahmen zu prüfen, sodass im Falle fehlschlagender Tests die Ursachen schneller erkannt werden können. Listing 7 zeigt exemplarisch die Überprüfung der benötigten Unlimited Strength JCE Policy Files, ohne die der eigentliche Testfall (Listing 8) nicht erfolgreich ausgeführt werden kann.

Listing 7

import javax.crypto.Cipher; import org.junit.*; public interface ITestJCE { @Test public default void testInstalledJCE() throws Exception {  // If JCE unlimited strength jurisdiction policy files are  // installed, Integer.MAX_VALUE will be returned. int keyLength = Cipher.getMaxAllowedKeyLength("AES"); Assert.assertEquals("Missing unlimited JCE!", Integer.MAX_VALUE,keyLength); } }

Listing 8

import javax.crypto.*; import org.j8unit.runners.J8Unit4; import org.junit.*; import org.junit.runner.*; @RunWith(J8Unit4.class) public class CryptoUtilTest implements ITestJCE { @Test public void testStrongAES() throws Exception { KeyGenerator keyGen = KeyGenerator.getInstance("AES");  // Requires unlimited JCE! keyGen.init(256); SecretKey key = keyGen.generateKey(); String plain = "Foobar"; String encrypted = CryptoUtil.encrypt(plain, key); String decrypted = CryptoUt...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Gewinner-Teams

Wir bieten Lizenz-Lösungen für Teams jeder Größe: Finden Sie heraus, welche Lösung am besten zu Ihnen passt.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang