Teil 5 von 5 aus der Serie "Maven to the rescue"

Integrationstests sind für Webservices besonder wichtig. Mit dem richtigen Maven-Setup kann man diese auch ausführen, ohne davon abhängig zu sein, welche Container auf dem jeweiligen Server zur Verfügung stehen.

Üblicherweise hat man auf den Entwickler- und Buildsystemen einen Projekt unabhängigen Webcontainer, wie Tomcat, der immer im Hintergrund läuft. Auf diesen wird dann während des Builds die Anwendung deployt, um anschließend Tests dagegen zu machen.
Das spart Ressourcen, wenn man lokal einen Container für mehrere Projekte verwendet. Gleichzeitig kann es aber störend auf einem Buildserver sein, wenn dieser Container z. B. regelmäßig neu gestartet wird und daher im Laufe eines Tests plötzlich nicht mehr erreichbar ist. Auch kann es sein, dass die Anwendung für eine andere Containerversion (z. B. Tomcat 8, Tomcat 9 oder Jetty) entwickelt wird und daher auf diesem getestet werden soll, wie es vor kurzem bei mir der Fall war.

Das org.codehaus.cargo.cargo-maven2-plugin unterstützt hierzu das Verwenden eines Embedded Containers. Man kann also für einen Integrationstest eine eigene Tomcat-Version instantiieren, dort beliebig Anwendungen installieren, und diese dann in Testcases ansprechen.
Damit kann man Abhängigkeiten auf den Container, der sonst auf dem Buildsystem verfügbar sein müsste, aufbrechen.

Um dies zu bewerkstelligen, muss man aber einige Schritte machen.

Ports

Zuerst benötigt man einen Port, auf dem der Embedded-Container lauschen kann. Genau genommen sind es mindestens drei Ports bei der Tomcat: HTTP, AJP und RMI. Da auf dem ausführenden Rechner die aktuelle Portbelegung verschieden sein kann, weil gerade andere Anwendungen die Standardports verwenden, benutzt man das org.codehaus.mojo.build-helper-maven-plugin, um dynamisch freie Ports zu ermitteln und diese in Maven-Properties abzulegen:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>build-helper-maven-plugin</artifactId>
    <version>3.0.0</version>
    <executions>
        <execution>
            <id>reserve-ports</id>
            <phase>initialize</phase>
            <goals>
                <goal>reserve-network-port</goal>
            </goals>
            <configuration>
                <portNames>
                    <portName>tomcatPort</portName>
                    <portName>tomcatAjp</portName>
                    <portName>cargoRmi</portName>
                </portNames>
            </configuration>
        </execution>
    </executions>
</plugin>

Nach der Pluginausführung stehen in Maven in den Properties ${tomcatPort}, ${tomcatAjp} und ${cargoRmi} die entsprechenden Portnummern zur Verfügung.

Embedded Cargo

Anschließend kann das Cargo-Plugin konfiguriert werden:

<plugin>
    <groupId>org.codehaus.cargo</groupId>
    <artifactId>cargo-maven2-plugin</artifactId>
    <version>1.7.4</version>
    <configuration>
        <container>
            <containerId>tomcat9x</containerId>
            <artifactInstaller>
                <groupId>org.apache.tomcat</groupId>
                <artifactId>tomcat</artifactId>
                <version>9.0.17</version>
            </artifactInstaller>
            <output>${project.build.directory}/tomcat9x-logs/container.log</output>
            <append>false</append>
            <log>${project.build.directory}/tomcat9x-logs/cargo.log</log>
        </container>
        <configuration>
            <home>${project.build.directory}/tomcat9x</home>
            <properties>
                <cargo.servlet.port>${tomcatPort}</cargo.servlet.port>
                <cargo.rmi.port>${cargoRmi}</cargo.rmi.port>
                <cargo.tomcat.ajp.port>${tomcatAjp}</cargo.tomcat.ajp.port>
                <cargo.tomcat.uriencoding>UTF-8</cargo.tomcat.uriencoding>
            </properties>
        </configuration>
        <deployables>
            <deployable>
                <groupId>mypackage</groupId>
                <artifactId>MyService</artifactId>
                <type>war</type>
            </deployable>
        </deployables>
        <skip>${skipTests}</skip>
    </configuration>
    <executions>
        <execution>
            <id>start-tomcat</id>
            <phase>pre-integration-test</phase>
            <goals>
                <goal>start</goal>
            </goals>
        </execution>
        <execution>
            <id>stop-tomcat</id>
            <phase>post-integration-test</phase>
            <goals>
                <goal>stop</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Hier wird die Tomcatversion 9.0.17 als Container verwendet. Diese wird dann tatsächlich durch das Cargo-Plugin von einem Server als Archiv heruntergeladen, lokal im target-Verzeichnis entpackt und ist damit lauffähig. Im Targetverzeichnis befinden sich dann auch die Logdateien, welche bei Fehlern sehr hilfreich sind. Im Beispiel oben sind die übergebenen Maven-Properties für die Ports ebenfalls erkennbar.
Unter deployables kann das aktuelle Modul, wenn es denn ein WAR-Archiv ist, installiert werden. Es können aber auch andere WAR-Module installiert werden. Beispielsweise abhängige Webservices, die vom aktuellen Modul verwendet werden. Besonders praktisch ist dies auch, wenn man einen Client testen möchte und hierzu den dazugehörigen Server in dem Embedded Container installiert.
Über die executions Blöcke wird gesteuert, wann der Container gestartet und wann heruntergefahren werden soll.

Damit ist man schon recht weit. Es fehlt noch das Bindeglied zwischen den Testcases und dem Embedded Container.

Tests

Die Testcases müssen nun den Port des Containers erfahren, damit sie diesen verwenden können. Am einfachsten platziert man zu diesem Zweck eine Properties in den Test-Resourcen:
test.properties

port=${tomcatPort}

Damit diese auch befüllt wird, darf man nicht vergessen, dass Filtering der Resourcen zu aktivieren:

<build>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <filtering>true</filtering>
        </resource>
    </resources>
    <testResources>
        <testResource>
            <directory>src/test/resources</directory>
            <filtering>true</filtering>
        </testResource>
    </testResources>
    ....

Im Testcase kann nun die Property ausgelesen und verwendet werden:

public class MyServiceIntegrationTest {
    private String port = ResourceBundle.getBundle("test").getString("port");
    private final String hostName = "http://localhost:" + port + "/MyService/";
    ...

Noch mehr Tests

Doch damit ist die Konfiguration leider noch nicht ganz erledigt. Üblicherweise hat man Unit- und Integrationstests und möchte deren Ausführung voneinander trennen. Die Integrationstests sollen in der Integrationsphase ausgeführt werden, während der der Embedded Container läuft. Die Unittests sollten keine solchen Abhängigkeiten haben und bei Bedarf Mocks einsetzen.

Hierzu bedient man sich am einfachsten eines Marker-Interfaces:

/**
 * Marker-Interface für Integrationstests<br>
 * Diese werden parallel zum Start des Webservice in einem Embedded Container
 * ausgeführt.
 */
public interface IntegrationTest { }

Diese Klasse wird nun an alle Integrationstests via @Category annotiert:

@Category(IntegrationTest.class)
public class MyServiceIntegrationTest {
    private String host = ResourceBundle.getBundle("test").getString("host");
    ...

Anschließend kann das Surefire-Plugin so konfiguriert werden, dass Unit- und Integrationstests zu verschiedenen Zeitpunkten ausgeführt werden.

<!-- Unittests -->
<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.22.2</version>
    <configuration>
        <skip>false</skip>
        <includes>
            <include>**/*.class</include>
        </includes>
        <!-- Marker-Interface -->
        <excludedGroups>mypackage.IntegrationTest</excludedGroups>
    </configuration>
</plugin>
<!-- Integrationtests -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>2.22.2</version>
    <configuration>
        <includes>
            <include>**/*.class</include>
        </includes>
        <!-- Marker-Interface -->
        <groups>mypackage.IntegrationTest</groups>
        <environmentVariables>
            <appversion>${project.version}</appversion>
            <previewhost>localhost</previewhost>
            <previewport>${tomcatPort}</previewport>
        </environmentVariables>
    </configuration>
    <executions>
        <execution>
            <id>failsafe-integration-tests</id>
            <phase>integration-test</phase>
            <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Damit sollte es dann auch klappen.

Series Navigation<< Aussagekräftige Buildversionsnummern in Maven