tech-trends

Hilfe bei Integrationstests

30 Min // 29-10-2018

IT-Spezialisten ist es vollkommen klar, dass das Testing von IT-Systemen wichtig ist. Wir können und müssen damit leben, dass wir genauso viele (wenn nicht mehr) Ressourcen ins Testing investieren wie in die Umsetzung von funktionalen und nicht funktionalen Anforderungen.

Wir sind alle damit einverstanden, dass effektive Tests und eine gute Testabdeckung die Voraussetzung für gute Code- und Produktqualität sind. Außerdem ist Refactoring ohne ausreichende Tests schwer vorstellbar.

Es gibt verschiedene Teststufen: Unit-Tests, Integrationstests, Systemtests, Abnahmetests. In den zwei ersten Phasen sind die Produktentwickler stark involviert. Für Unit-Tests existieren Frameworks wie JUnit 4/5, Mocki- to oder AssertJ [1]. Mit diesen Tools lassen sich kurze und elegante Tests schnell erstellen. Die Frameworks sind gut dokumentiert und einfach zu erlernen. Wenn es sich um Integrationstests handelt, ist es allerdings nicht mehr ganz so einfach. Mithilfe von Integrationstests wollen wir überprüfen, ob das System ein korrektes Verhalten zeigt, wenn es auf dem Applikationsserver läuft und in verschiedene Umsysteme und Datenbanken eingebunden ist.

Das funktioniert, wenn wir beispielsweise eine öffentliche Schnittstelle (REST/SOAP) testen wollen. In diesem Fall generieren wir einen Java-Client, schicken eine Anfrage an das System und kontrollieren, ob das System die erwartete Antwort zurückliefert. Durch Datenbanktools (etwa DbUnit) kann man feststellen, ob der Datenbankstand nach dem Serveraufruf korrekt ist.

Aber wie kann man interne Services testen, die keine Webschnittstelle zur Verfügung stellen? Hier meinen wir nicht die Unit-Tests, die wir alle kennen. Wie testen wir so einen internen Service auf dem Applikationsserver, wenn er auf andere Systeme und Datenbanken zugreift? Könnten Mocks dabei behilflich sein? Das Mocken von externen Aufrufen (insbesondere bei JPA) ist sehr aufwendig und in manchen Fällen sinnlos, denn letztendlich ist dann alles gemockt und es bleibt nichts zum Testen.

Welche Frameworks stehen zur Verfügung? Es gibt Arquillian, aber das hat in manchen Situationen Nachteile, die das Testen etwas schwieriger machen. Wenn das System groß ist oder aus mehreren Subsystemen besteht, kann der Zusammenbau des Deployments eine

Herausforderung sein. Daneben macht es ein neues Deployment für jede Testklasse. Wenn es um Hunderte von Testklassen geht und das Deployment relativ lange dauert, ist das nicht mehr effizient.

Man könnte argumentieren, dass solche großen Deployments ohnehin Vergangenheit seien und alles auf Microservices umgestellt werden solle. Wir wissen jedoch, dass das aus verschiedensten Gründen nicht immer möglich ist. Auch gibt es bestehende Applikationen, die man warten und weiterentwickeln muss. Was könnte man einsetzen, wenn man Tests gegen eine bereits installierte Applikation ausführen will? Für eines unserer Projekte suchten wir ein solches Tool und haben WarpUnit [2] gefunden.

//

Wenn Web Services nicht mehr reichen

Wie kam es überhaupt dazu? In diesem Projekt könnten wir eigentlich alles über Web Services testen. Doch eines Tages bekamen wir die Aufgabe, einen WorkCageService zu implementieren, der alle noch nicht erledigten Aufgaben eines Benutzers zurückliefern muss. Dazu erstellten wir einen Web Service (Listing 1) und einen

Domain-Service (Listing 2). Aufgaben (Tasks) bestehen aus offenen Terminen (Appointments) und Dokumenten. Die Logik haben wir in zwei private Methoden ausgelagert: getOpenAppointments und getOpenDocuments.

In unserer Testimplementierung liefert die Methode getOpenAppointments immer einen Testtermin zurück, wobei getOpenDocuments einige Datenbankabfragen ausführt und Dokumente aus der Datenbank zurückgibt.

Sofort stellten wir uns die Frage: Wie testen wir das? Wir brauchen zwei Tests, einen pro Methode. Wir hatten aber nur die Web-Service-Schnittstelle zur Verfügung.

Somit entstanden zwei unschöne Tests, in denen jedes Mal eine Methode umsonst aufgerufen wurde. So geht das nicht. Wir wollten testen können wie in Listing 3.

Wir wollten allerdings nichts mocken, im Gegenteil.

Es musste ein richtiger Integrationstest sein, mit Datenbankabfrage und einem externen Serviceaufruf. Die Lösung lautete schließlich WarpUnit. Es ist ein leichtgewichtiges Open-Source-Framework für Gray-Box-Testing von Java-EE-Applikationen. Das Testen erfolgt gegen eine laufende Applikation. Somit kann man auf den Zusammenbau des Deployments im Test verzichten. Es reicht, das warpunit-insider.war ins EAR-Paket zu inkludieren.

WarpUnit funktioniert so: Die Tests laufen als Standalone-JUnit-Tests. Auf dem Server wird nur der Code ausgeführt, der wirklich zum Serverteil gehört, insbesondere CDI, EJB, JPA. Während des Tests wird der Bytecode dieses Teils serialisiert, über eine REST-Schnittstelle

an warpunit-insider geschickt und auf dem Server ausgeführt. Das Ergebnis wird zurückgegeben und lässt sich im Test mit allen möglichen Mitteln überprüfen.

An dieser Stelle sei erwähnt, dass warpunit-insider keinesfalls in Produktion aktiv sein darf. Sonst gibt es eine Backdoor, über die beliebiger Code ausgeführt werden kann. Das lässt sich sehr einfach einstellen: WarpUnit ist nur dann aktiv, wenn das System-Property warpunit.enabled auf true gesetzt ist.

Zwei Arten zu testen

Unser EAR-Paket mit warpunit-insider ist deployt. Wie schreiben wir einen Test? Je nach Geschmack bietet das Framework zwei Möglichkeiten. Schauen wir uns Möglichkeit 1 an: Um getOpenAppointments zu testen, erstellt man ein Proxy-Interface, das genau beschreibt, wie die Daten zu beschaffen sind:

public interface WorkingCageInsider {
public List<Request> getOpenAppointments();
}

Dazu erstellt man selbstverständlich eine Implementierung, die tatsächlich auf dem Server ausgeführt wird (Listing 4). (Listing 5) beschreibt unseren ersten WarpUnit-Test. Mit GateBuilder konfigurieren wir WarpUnit, schicken die Proxy-Klasse an den Server, lassen diese dort laufen und bekommen das Ergebnis. Das kann jetzt mit beliebigen Mitteln, beispielsweise AssertJ, überprüft werden.

Listing 1

@WebService
public class WorkingCageServiceImpl implements
WorkingCageService {
  @Inject
  private WorkingCage workingCage;
  public List<Task> getOpenTasks() {
  return workingCage.getOpenTasks();
  }
}
Listing 2

public class WorkingCage {
  public List<Task> getOpenTasks(){
    List<Task> openTasks = new ArrayList<>();
    openTasks.addAll(getOpenAppointments());
    openTasks.addAll(getOpenDocuments());
    return openTasks;
  }
}
Listing 3

@Inject
WorkingCage workingCage;

@Test
public void testOpenRequests() {
  List<Request> openDocuments = workingCage.
getOpenDocuments();
  assertThat(...);
}
Listing 4

public class WorkingCageInsiderImpl implements
WorkingCageInsider {
  @Inject
  WorkingCage workingCage;

  public List<Request> getOpenAppointments () {
    return workingCage.getOpenAppointments ();
  }
}
Listing 5

public class ProxyStyleWorkingCageTest {
  @Test                                                              System.out.println("This is printed in the JUnit
  public void testWorkingCageWithProxy() {                           test log");

WorkingCageInsider proxyGate = WarpUnit.                             List<Appointment> appointments = proxyGate.
builder()                                                            getOpenAppointments();
  .primaryClass(WorkingCageInsiderImpl.class)                        Assert.assertEquals(1, appointments.size());
  .includeInterface(true)                                             }
  .createProxyGate(WorkingCageInsider.class);                        }

Ein bisschen eleganter geht es mit Möglichkeit 2:

Lambdas (Listing 6). Wir testen damit genau die eine Methode, die wir testen wollen und zusätzlich sind alle Java-EE-Features dabei.

Datenbanken

Stellen Sie sich vor, dass der DocumentService unterschiedliche Dokumententypen liefern kann. Davon ist abhängig, was in der Datenbank steht. In unserem Beispiel haben wir den Initialstand aus Tabelle 1 und 2.

Die Geschäftsregel, die wir testen wollen, ist einfach:

Medizinische Dokumente mit dem Status open werden nur dann zurückgegeben, wenn Preference med.open = true.

In unserer Testimplementierung gibt es keine Dokumente außer medizinischen. Entsprechend läuft der Test in (Listing 7) dargestellte Test ab.

Wir wollen folgenden Fall testen: Wenn es in der Datenbank einen Eintrag med.open = true gibt, soll genau ein Dokument zurückgeliefert werden. Dafür haben wir eine Hilfsklasse EntityManipulator geschrieben. Sie ermöglicht, einfache Datenbankoperationen wie find, update, insert im Test mithilfe von WarpUnit auszuführen.

Die Klasse implementiert das Interface Closeable und lässt sich über try with resource verwenden. Die Methode close() bereinigt geänderte Daten: Neue Einträge werden gelöscht, geänderte Daten werden zurückgesetzt. Allerdings ist zu beachten, dass komplexe Entitäten mit Beziehungen (noch) nicht unterstützt werden.

Wie unser Test dann aussieht, zeigt sich in (Listing 8). Schließlich wollen wir noch einen Test haben, in dem wir med.restricted dazu aktivieren und beide Dokumente zurückbekommen (Listing 9).

Listing 6

public class LambdaStyleTest {
  @Inject
  WorkingCage workingCage;

  @Test
  public void testWorkingCage() {
    WarpGate gate = WarpUnit.builder()
      .primaryClass(LambdaStyleTest.class)
      .createGate();

List<Appointment> appointments = gate.warp(() -> {
      return workingCage.getOpenAppointments();
    });
    Assert.assertEquals(1, appointments.size());
    }
  }
Listing 7

@Test
public void testNoDocuments() {
  WarpGate gate = WarpUnit.builder()
    .primaryClass(DBTest.class)
    .createGate();

  List<Document> documents = gate.warp(() -> {
    return workingCage.getOpenDocuments();
  });

    //Keine Dokumente werden zurückgeliefert,
    //da es kein aktives Preference gibt
    Assert.assertEquals(0, documents.size());
  }

//

WAR-Deployments testen

Bis zu diesem Zeitpunkt ging darum EAR-Deployments zu testen, in denen warpunit-insider.war mitgepackt wurde. WAR-Artefakten lassen sich auf die gleiche Weise testen. Es reicht, warpunit-core.jar ins WAR-Paket zu inkludieren. Wir wollen natürlich unsere Webapplikation nicht mit WarpUnit produktiv betreiben, wir brauchen es ausschließlich für die Integrationstests. Eine Möglichkeit wäre es, eine Testversion zu bauen. Mit Maven-Profilen lässt sich diese Aufgabe lösen.

Für Testzwecke haben wir ein working-cage-war-Projekt angelegt. In der pom.xml des Projekts definiert man die Version beispielsweise so:

<version>${projectVersion}</version>
<properties>
<projectVersion>alpha-SNAPSHOT</projectVersion>
</properties>

Ein Profil in der entsprechenden Art und Weise findet sich in (Listing 10).

Der letzte Schritt lautet mvn install -Pwarpunit und … fertig. Eine Kleinigkeit gibt es noch zu beachten:

Der WarpUnit-REST-Service hat im neuen Paket einen anderen URL bekommen. Tests müssen diesen kennen, um mit warpunit-insider kommunizieren zu können.

Dies lässt sich einmal global einstellen, beispielsweise so:

@BeforeClass
public static void setupBeforeClass() {
WarpUnit.DEFAULT_REMOTE_ENDPOINT_URL = "http://localhost:8080/
working-cage-war";
}
Listing 8

@Test
public void testOpenDocuments() throws IOException {
try (EntityManipulator manipulator = new EntityManipulator()) {

  // Instanziieren von JPA-Entity Preference.
  Preference preference = new Preference();
  preference.setPreferenceKey("med.open");
  preference.setPreferenceValue("true");

  // Hier wird die Preference-Instanz an Server gesendet.
  // und von WarpUnit-Insider über JPA EntityManager.
  // in die Datenbank eingespielt.
  manipulator.insertEntity(preference);

  WarpGate gate = WarpUnit.builder()
    .primaryClass(DBTest.class)
    .createGate();

  List<Document> documents = gate.warp(() -> {
    return workingCage.getOpenDocuments();
  });

  // Das Preference med.open=true.
  // Somit wird ein Dokument geliefert.
Assert.assertEquals(1, documents.size());

  // Der erstellte Preference-Eintrag wird am Ende
  // durch close() entfernt
    }
  }
Listing 9

@Test
  public void testOpenAndRestrictedDocuments() throws Exception {
  try (EntityManipulator manipulator = new EntityManipulator()) {
    // Ein Preference-Eintrag wird in der Datenbank erstellt.
  Preference openPreference = new Preference();
  openPreference.setPreferenceKey("med.open");
  openPreference.setPreferenceValue("true");
  manipulator.insertEntity(openPreference);

  // Ein bestehender Preference-Eintrag wird aus der Datenbank
  // mithilfe einer HQL-Query geholt.
  Preference restrictedPreference = manipulator.
loadEntityByQuery("SELECT p FROM Preference p where preferenceKey = 'med.restricted'", Preference.class);
  restrictedPreference.setPreferenceValue("true");

  // Hier wird die Preference-Instanz an Server gesendet.
  // EntityManimulator ruft am Server die Methode merge() auf.
  // Dadurch wird der Eintrag auch in der Datenbank aktualisiert.
  manipulator.updateEntity(restrictedPreference);

WarpGate gate = WarpUnit.builder()
  .primaryClass(DBTest.class)
  .createGate();

  List<Document> documents = gate.warp(() -> {
  return workingCage.getOpenDocuments();
  });

// Die beiden Preferences med.open und med.restricted sind true.
  // Somit werden beide Dokumente zurückgeliefert.
  Assert.assertEquals(2, documents.size());

  // Der neue Preference-Eintrag wird am Ende entfernt
  // Der aktualisierte Preference-Eintrag wird am Ende zurückgesetzt
    }
  }
Listing 10

<profile>
  <id>warpunit</id>
  <dependencies>
  <dependency>
    <groupId>org.dcm4che</groupId>
    <artifactId>warpunit-core</artifactId>
    <version>alpha-SNAPSHOT</version>
  </dependency>
  </dependencies>
  <properties>
    <projectVersion>alpha-TEST-ONLY-SNAPSHOT</projectVersion>
    </properties>
  </profile>

//

Den Test testen

WarpUnit dient dazu, Tests gegen eine bereits installierte Applikation auszuführen.

Wer selbst testen möchte, kann dies in vier Schritten tun:

  1. git clone https://github.com/eerofeev/WarpUnit.git
  2. mvn install -DskipTests
  3. Das working-cage-ear-alpha-SNAPSHOT.ear auf WildFly [3] oder JBoss EAP deployen
  4. Tests aus dem Maven-Projekt working-cage-test

ausführen.

Java 8 und Java EE 7 sind die Voraussetzungen. Getestet wurde auf Wildfly 10.1.0.Final. Das ursprüngliches WarpUnit-Repository steht auf GitHub zur Verfügung, alle im Artikel beschriebenen Beispiele sind dort natürlich zu finden.

// Autor

Egor Erofeev & Dr. Roman Khazankin