Test & Quality

Testen ohne neu zu bauen: Java-8-Features in JUnit-Tests

Sascha Zak, Stefan Gasterstädt

© iStock / AndSim

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.

import org.junit.*;

public class ReusableTests {
  public static void testFoobar1(Foobar sut) {
    // ...
    Assert.assertXXX(...);
  }

  public static void testFoobar2(Foobar sut) {
    // ...
    Assert.assertXXX(...);
  }
}
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);
  }
}
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(...);
  }
}
public class OldStyleTest2 extends ExtendableTests {
  @Override
  protected Foobar createNewSUT() {
    return new Foobar();
  }
}

Die in Java 8 erschienenen Sprachfeatures der default-Methoden in Interfaces, 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.

Lesen Sie auch: Lambda goes Alpha: Einführung in Architektur und API von JUnit 5

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.

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(...);
  }
}
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. Listings 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.

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);
  }
}
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 = CryptoUtil.decrypt(encrypted, key);
    Assert.assertNotEquals(plain, encrypted);
    Assert.assertEquals(plain, decrypted);
  }
}

In den meisten Fällen wird ein Wiederverwenden von Tests auf jeweils unterschiedliche Testsubjekte abzielen – analog zu der Wiederverwendung per expliziten Methodenaufruf mit vorab erstelltem SUT (Listing 2). Für wiederzuverwendende Interfacemethoden, bei denen ein solcher expliziter Aufruf bewusst nicht stattfinden soll, muss eine alternative SUT-Bereitstellung erfolgen. Zwar ist es für ein Interface nicht möglich, auf Klassen-Member zuzugreifen – unabhängig davon, ob per @Before-Methode aufgefrischt oder per parametrisiertem Test initiiert –, aber natürlich kann stattdessen eine entsprechende SUT-Factory-Methode eingefordert werden. Für Tests, die lediglich eine frische SUT-Instanz benötigen, empfiehlt sich das Verwenden von J8UnitTest (Listings 9, 10). Natürlich kann aber auch jede andere Variante umgesetzt werden, die für default-Methoden anwendbar ist. Eine individuell angepasste Vorgehensweise ist in jedem Fall notwendig, wenn ein default-Test mehr als eine einzelne SUT-Abhängigkeit aufweist. Listing 11 zeigt exemplarisch, dass J8UnitTest auch problemlos in einem parametrisierten Kontext nutzbar ist.

import org.j8unit.J8UnitTest;
import org.junit.*;

public interface ITest2 extends J8UnitTest<Foobar> {
  @Test
  public default void testFoobar1() {
    Foobar sut = createNewSUT(); // specified within J8UnitTest
    // ...
    Assert.assertXXX(...);
  }

  @Test
  public default void testFoobar2() {
    Foobar sut = createNewSUT(); // specified within J8UnitTest
    // ...
    Assert.assertXXX(...);
  }
}
import org.j8unit.runners.J8Unit4;
import org.junit.*;
import org.junit.runner.*;

@RunWith(J8Unit4.class)
public class Test2 implements ITest2 {
  @Override // specified within J8UnitTest
  public Foobar createNewSUT() {
    return new Foobar();
  }

  // Optional further test class specific test methods ...
}
import java.util.Arrays;
import org.j8unit.runners.J8Parameterized;
import org.j8unit.runners.parameterized.J8BlockJUnit4ClassRunnerWithParametersFactory;
import org.junit.*;
import org.junit.runner.*;
import org.junit.runners.Parameterized.*;

@RunWith(J8Parameterized.class)
@UseParametersRunnerFactory(J8BlockJUnit4ClassRunnerWithParametersFactory.class)
public class Test3 implements ITest2 {
  @Parameters
  public static Iterable<Object[]> data() {
    Supplier<Foobar> fac = Foobar::new;
    return Arrays.asList(new Object[][] { { fac } });
  }

  @Parameter(0)
  public Supplier<Foobar> factory;

  @Override // specified within J8UnitTest
  public Foobar createNewSUT() {
    return this.factory.get();
  }

  // Optional further test class specific test methods ...
}

Eigene J8Unit-Testhierarchien

Neben der generellen Wiederverwendbarkeit bieten die J8Unit-Test-Interfaces auch die Möglichkeit, Testfälle hierarchisch zu verteilen. Dies ist insbesondere dann sinnvoll, wenn sich die Testfällestruktur an einer bereits vorliegenden API-Interfaces-Hierarchie orientieren.

Lesen Sie auch: Verteilte Systeme automatisiert testen: Geht das? Und wenn ja, wie?

Für den folgenden Abschnitt gehen wir davon aus, dass eine Eigenimplementierung des List-Interface vorgenommen wurde. Zudem soll diese List-Implementierung der Oracle-Definition von unmodifiable genügen. Abbildung 1 zeigt die Hierarchie des List-Interface sowie die analoge Hierarchie der Testfälle. Zudem wurden Tests, welche nur für unmodifiable Listen gelten durch weitere J8Unit-Testinterfaces ergänzt.

zak_j8unit_1

Abb. 1: Hierarchie von „List“- und analogem „(Unmodifiable)ListTests“-Interface

Das Testinterface IterableTests beinhaltet hierbei Tests, die lediglich die Iterator-Eigenschaft erfordern. Die Testinterfaces CollectionTests und ListTests benennen analog Tests, die auf der Collection– bzw. List-Eigenschaft des SUT beruhen. In jedem Fall verwenden diese Tests nur Query-Operationen, da die Ausführbarkeit von modifizierenden Operationen nicht allgemeingültig vorausgesetzt werden kann. Insbesondere sind diese Operationen dann nicht ausführbar, wenn es sich um unmodifiable Listen handelt. Es kann unterstellt werden, dass entsprechende Methodenaufrufe stets mit einer UnsupportedOperationException abbrechen. Tests dieser Art befinden sich in den entsprechenden Testinterfaces UnmodifiableIterableTests, UnmodifiableCollectionTests und UnmodifiableListTests.

Um nun die Eigenimplementierung zu testen, bedarf es lediglich einer konkreten Testklasse und der Bezugnahme auf die benötigten Testinterfaces. Listing 12 verwendet hierbei eine parametrisierte Testausführung, um unterschiedlich befüllte MyUnmodifiableList-Instanzen zu benutzen. Eine Ausführung der Testklasse via Eclipse führt zur Darstellung gemäß Abbildung 2.

Der einzelne abgebrochene Testfall (skipped) ist darauf zurückzuführen, dass die Testmethode testUnsupportedRemoveViaIterator() vernünftig implementiert wurde und per JUnit-Assume erkennt, dass kein Iterator-remove() stattfinden kann, wenn das fehlschlagende Iterator-hasNext() bereits vorab ein leeres Iterable-SUT detektiert (Listing 13). Ergänzend ist in Abbildung 2 dargestellt, dass die Methode clear() in allen Fällen unerlaubterweise ausgeführt werden konnte und somit ein Fehler in der Implementierung vorliegt.

import static java.util.Arrays.asList;
import java.util.*;
import org.j8unit.runners.J8Parameterized;
import org.j8unit.runners.parameterized.J8BlockJUnit4ClassRunnerWithParametersFactory;
import org.junit.runner.*;
import org.junit.runners.Parameterized.*;

@RunWith(J8Parameterized.class)
@UseParametersRunnerFactory(J8BlockJUnit4ClassRunnerWithParametersFactory.class)
public class MyUnmodifiableListTest implements UnmodifiableListTests<List<Object>, Object> {
  @Parameters(name = "{index}: {1}")
  public static Iterable<Object[]> data() {
    return asList(new Object[][] {
{ new MyUnmodifiableList<>(), "empty MyUnmodifiableList" },
{ new MyUnmodifiableList<>(asList("Foobar")), "singleton MyUnmodifiableList" },
{ new MyUnmodifiableList<>(asList("Foo", "Bar")), "2-tuple MyUnmodifiableList" }
    });
  }

  @Parameter(0)
  public List<Object> instance;

  @Parameter(1)
  public String display;

  @Override
  public List<Object> createNewSUT() {
    return this.instance;
  }
}
zak_j8unit_2

Abb. 2: Ausführung der Testklasse „MyUnmodifiableListTest“

import java.util.Iterator;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Test;

public interface UnmodifiableIterableTests<UI extends Iterable<E>, E> extends IterableTests<UI, E> {

  @Test(expected = UnsupportedOperationException.class)
  public default void testUnsupportedRemoveViaIterator() {
    UI sut = createNewSUT();
    Iterator<E> iterator = sut.iterator();
    Assert.assertNotNull(iterator);
    Assume.assumeTrue("The given Iterable subject returned an Iterator without any next element.", iterator.hasNext());
    iterator.next();
    iterator.remove();
  }

}

Genau genommen muss die Eigenimplementierung nicht nur den Anforderungen an eine unmodifiable List genügen, sondern auch den allgemeinen Anforderungen an Object. Für einen entsprechenden Test wäre es möglich, das IterableTests-Interface von einem entsprechenden ObjectTests abzuleiten. Doch dies stünde nicht im Einklang mit der Hierarchie der API-Interfaces. Vielmehr ist die Klasse MyUnmodifiableList selbst (implizit) von der Oberklasse Object abgeleitet, sodass sich eine analoge Erweiterung der Testklasse MyUnmodifiableListTest anbietet:

@RunWith(J8Parameterized.class)
@UseParametersRunnerFactory(J8BlockJUnit4ClassRunnerWithParametersFactory.class)
public class MyUnmodifiableListTest implements ObjectTests<List<Object>>, UnmodifiableListTests<List<Object>, Object> {
  [...]
}

Eine Testausführung deckt nun auch generelle Annahmen an die toString– und equals-Methode ab (Abb. 3).

zak_j8unit_3

Abb. 3: Ausführung der Testklasse „MyUnmodifiableListTest“

Erweiterte Testframeworks

Selbstverständlich lässt sich J8Unit auch in Kombination mit bestehenden, auf JUnit aufbauenden Testframeworks verwenden. Einzige Voraussetzung ist, dass die entsprechenden Runner erweitert werden können, sodass sie das erweiterte Testklassenmodell aus J8Unit verwenden. Exemplarisch zeigt Listing 14 einen Testfall, der unter Verwendung einer erweiterten Spring-Runner-Klasse den Spring-Dependency-Injection-Mechanismus für das SUT in Kombination mit dem J8Unit-Test-Interface aus Listing 10 verwendet. Hierbei ist zu beachten, dass die Konfiguration des ApplicationContext (im Beispiel annotationsbasiert in der Klasse TestContext) vorgibt, ob es sich bei dem injizierten SUT um eine jeweils neue erzeugte oder aber eine Singleton-Instanz handelt. Der erweiterte Spring-Runner ist bereits Bestandteil eines zusätzlichen J8Unit-Moduls und lässt sich mit der Abhängigkeit einbinden:

<dependency>
  <groupId>org.j8unit</groupId>
  <artifactId>spring</artifactId>
  <version>4.1.7-RELEASE</version>
  <scope>test</scope>
</dependency>

Erweiterungen für andere Testframeworks lassen sich ähnlich leicht umsetzen.

import org.j8unit.showcase.jm.*
import org.j8unit.spring.runners.J8SpringUnit4;
import org.j8unit.spring.showcase.TestContext;
import org.junit.*;
import org.junit.runner.*;
import org.springframework.text.context.ContextConfiguration;

@RunWith(J8SpringUnit4.class)
@ContextConfiguration(classes={TestContext.class})
public class SpringTest implements ITest2 {
  @Resource
  private Foobar sut; // injected

  @Override
  public Foobar createNewSUT() {
    return sut;
  }

  @Test
  public void testFoobar3() {
    Foobar sut = createNewSUT();
    // ...
    Assert.assertXXX(...);
  }
}

J8Unit für API-Designer

Das J8Unit-Testklassenmodell in Verbindung mit den entsprechend erweiterten Runner-Klassen eröffnet insbesondere Entwicklern und Designern öffentlicher Schnittstellen (APIs) neue Möglichkeiten, die Spezifikation ihrer Schnittstellen zu formulieren. Diese waren bisher auf die textuelle Beschreibung in Kommentaren und Javadoc oder seltener auch Annotationen beschränkt. Die textuelle Beschreibung birgt dabei stets eine sprachlich bedingte Unschärfe – so unterscheidet etwa Oracle in seinem Collection-API sehr fein zwischen den Begriffen immutable und unmodifiable. Doch oft haben API-nutzende Programmierer ein anderes Verständnis solcher Begleitbegriffe oder nehmen diese Definitionen erst gar nicht wahr. Mit J8Unit-Test-Interfaces lassen sich Schnittstellenverträge nun auch ohne Kenntnis einer konkreten Ausprägung technisch spezifiziert und dem API-Nutzer modular und komfortabel zur Verfügung stellen. Auf diese Weise lassen sich Implementierungen von Interfaces oder Erweiterungen von Klassen eines API ohne nennenswerten Mehraufwand auf Vertragskonformität im Sinne des Designers überprüfen.

Lesen Sie auch: 10 Open Source Testing Tools: Welches passt zu mir?

Im folgenden Beispiel nehmen wir an, ein solcher Designer erstellt ein API zur Beschreibung von geometrischen Formen. Als Basis aller Formen definiert er das Interface Shape mit den Methoden getPerimeter() zur Berechnung des Umfangs und getArea() zur Berechnung des Flächeninhalts:

package org.framework.shapes;

public interface Shape {
  public double getPerimeter();
  public double getArea();
}

Als eine konkrete Implementierung stellt er die Klasse Rectangle gemäß Listing 15 zur Beschreibung eines Rechtecks bereit. Neben der textuellen Beschreibung des Verhaltens ist er mit J8Unit nun auch in der Lage, spezielle Anforderungen an ein Rechteck in Form eines wiederverwendbaren Testinterface zu spezifizieren (Listing 16). Eine Anforderung kann etwa die Eigenschaft sein, dass sich der Flächeninhalt eines Rechtecks verdoppelt, wenn sich die Länge einer seiner Seiten verdoppelt. Dieses lässt sich, wie später noch gezeigt wird, gemeinsam mit dem API als eigenes Artefakt anderen Entwicklern zur Verfügung stellen.

package org.framework.shapes;

public class Rectangle implements Shape {

  private double width;
  private double height;
  
  public Rectangle(double width, double height) {
    this.width = width;
    this.height = height;
  }

  public double getWidth() {
    return this.width;
  }

  public void setWidth(double width) {
    this.width = width;
  }

  public double getHeight() {
    return this.height;
  }

  public void setHeight(double height) {
    this.height = height;
  }

  @Override
  public double getPerimeter() {
    return 2 * (this.width + this.height);
  }

  @Override
  public double getArea() {
    return this.width * this.height;
  }
}
package org.framework.shapes;
import org.j8unit.J8UnitTest;
import org.junit.*;

public interface RectangleTest<R extends Rectangle>
extends J8UnitTest<R> {

  // ...

  @Test
  public default void testScalingAreaByScalingWidth() {
    R sut = createNewSUT();
    double oldArea = sut.getArea();
    sut.setWidth(2 * sut.getWidth());
    double newArea = sut.getArea();
    Assert.assertEquals(newArea, 2 * oldArea, 0.0);
  }

  @Test
  public default void testScalingAreaByScalingHeight() {
    R sut = createNewSUT();
    double oldArea = sut.getArea();
    sut.setHeight(2 * sut.getHeight());
    double newArea = sut.getArea();
    Assert.assertEquals(newArea, 2 * oldArea, 0.0);
  }
}

Nehmen wir weiterhin an, ein Entwickler nutzt die Shape-API, in dem er diese als Abhängigkeit in sein Projekt einbindet. Für seine Zwecke benötigt er allerdings ein Quadrat, und da es zunächst wie ein Spezialfall eines Rechtecks erscheint, erbt seine Klasse Square in Listing 17 kurzerhand von Rectangle. Für den Unit Test der Klasse Square bindet er zusätzlich zu J8Unit das Shape-API-Testartefakt als Abhängigkeit (mit Test-Scope) in das Projekt ein und kann sofort gemäß Listing 18 per implements RectangleTest den bereitgestellten Unit Test für Rechtecke auf seine Quadratimplementierung anwenden. Völlig überraschend stellt er wie in Abbildung 4 dargestellt fest, dass sich Quadrate in Bezug auf die Skalierung ihres Flächeninhalts anders verhalten als Rechtecke.

package org.example.shapes;
import org.framework.shapes.Rectangle;

public class Square extends Rectangle {
  public Square(double width) {
    super(width, width);
  }

  @Override
  public void setHeight(double height) {
    super.setHeight(height);
    super.setWidth(height);
  }

  @Override
  public void setWidth(double width) {
    super.setHeight(width);
    super.setWidth(width);
  }
}
package org.example.shapes;
import org.framework.shapes.RectangleTest;
import org.j8unit.runners.J8Unit4;
import org.junit.*;
import org.junit.runner.*;

@RunWith(J8Unit4.class)
public class SquareTest implements RectangleTest<Square> {
  @Override
  public Square createNewSUT() {
    return new Square(2.0);
  }

  @Test
  public void testWidthAndHeightEqual() {
    Square sut = createNewSUT();
    Assert.assertEquals(sut.getWidth(), sut.getHeight(), 0.0);
  }
}
zak_j8unit_4

Abb. 4: Testergebnis SquareTest

J8Unit-Tests bereitstellen

Um die für ein API definierten J8Unit-Testinterfaces nachnutzen zu können, müssen diese im Java-Classpath verfügbar sein. Der Einsatz von Dependency-Management-Werkzeugen wie Maven erfordert hierbei ein besonderes Vorgehen, da Unit Tests üblicherweise strikt vom Produktivcode getrennt und in einem eigenen Quellenverzeichnis (in Maven src/test/java) untergebracht sind. Damit sind sie standardmäßig nicht Bestandteil des Deployment-Artefakts, und der API-Nutzer kann sie weder sehen noch nutzen. Um Testcode dennoch verfügbar zu machen, bietet das Maven-JAR-Plug-in ein besonderes goal. Es erlaubt, Testcode als eigenes Artefakt bereitzustellen und auf Nutzerseite über eine übliche Dependency-Deklaration (mit Scope test) zu verwenden. Listing 19 zeigt eine entsprechende Maven-Konfiguration für den API-Designer, um seinen Testcode bereitzustellen.

Wenn der API-Designer internen und bereitzustellenden Testcode unterscheiden möchte, ist es etwa möglich, diesen internen Testcode in einem separaten Namensraum zu halten und bei der Test-JAR-Erzeugung per Filter auszublenden. Dies wird im <configuration>-Block exemplarisch für das Package internal gezeigt:

<dependency>
  <groupId>groupId</groupId>
  <artifactId>artifactId</artifactId>
  <type>test-jar</type>
  <version>version</version>
  <scope>test</scope>
</dependency>
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-jar-plugin</artifactId>
  <version>2.6</version>
  <executions>
    <execution>
      <goals>
      <goal>test-jar</goal>
      </goals>
      <!-- Optional: Exklusion von internem Testcode --> 
      <configuration>
        <excludes>
          <exclude>**/internal/</exclude>
        </excludes>
      </configuration>
    </execution>
  </executions>
</plugin>

Fazit und Ausblick

Wie der Artikel gezeigt hat, ist es mit dem Erscheinen des Java-8-Sprachfeatures der default-Methoden in Interfaces möglich, Testcode ohne Mehraufwand wiederzuverwenden. Im Zusammenhang mit JUnit bedarf es einiger Erweiterungen, die das Projekt J8Unit bereitstellt. JUnit selbst sieht seit geraumer Zeit den Einbau entsprechender Java-8-Features vor.

Doch bis dahin kann bereits jetzt mit J8Unit jeder API-Designer die Definition seiner Schnittstellen nicht nur textuell beschreiben, sondern durch begleitende Unit Tests schärfen.

Diese Tests sollten bestenfalls als separate Artefakte bereitgestellt werden – ähnlich wie die oftmals ebenso erzeugten Sources- und Javadoc-JARs. Die Autoren konnten diese Vorgehensweise bereits erfolgreich in Projekten bei der eHealth Experts GmbH sowie der adesso AG umsetzen.

Darüber hinaus wäre es eine hervorragende Bereicherung, wenn Java selbst auf ähnliche Art API-begleitende Tests bereitstellen würde. J8Unit baut gerade ein solches, an der Hierarchie der Java-Standardbibliothekschnittstellen orientiertes, Testcode-Repository auf, das in Analogie zu Listing 13 hervorragend in bestehende Projekte eingebunden werden kann. Über Möglichkeiten zur Unterstützung an diesem Repository informiert die J8Unit-Webseite.

Verwandte Themen:

Geschrieben von
Sascha Zak
Sascha Zak
Sascha Zak ist Senior Software Engineer bei der adesso AG im Compentence Center Telematik in Berlin. Schwerpunktmäßig beschäftigt er sich mit Architektur und Entwicklung von webbasierten Systemen sowie Anwendungen im Umfeld von Smartcards und der elektronischen Gesundheitskarte.
Stefan Gasterstädt

Stefan Gasterstädt ist Leiter Entwicklung der eHealth Experts GmbH, fordert und fördert in den von ihm verantworteten Softwareprojekten die gleichwertige Betrachtung von Code und Testcode. Seine zahlreichen Erfahrungen bringt er auch als begleitender Coach in Kundenprojekte ein.

Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: