Die Krux mit Spring Boot und dem maven-failsafe-plugin

17.07.2020

In bestimmten Szenarien vertragen sich @SpringBootTests und das maven-failsafe-plugin nicht besonders gut. Die Hintergründe dazu, und wie ihr die damit verbundenen Probleme lösen könnt, erfahrt ihr hier.

 

Wer Anwendungen mit Spring Boot entwickelt, schreibt üblicherweise (bzw. hoffentlich! ;-) auch @SpringBootTests. Da hierfür der Spring-ApplicationContext hochgefahren wird, handelt es sich dabei um Integrationstests.

Maven stellt dafür eigens die Phase „integrationtest“ bereit. Dort sorgt das maven-failsafe-plugin dafür, dass alle Testklassen ausgeführt werden, deren Namen mit „IT“ beginnen oder enden. Demgegenüber steht die vorgelagerte Phase „test“, in der die Unit-Tests vom maven-surefire-plugin ausgeführt werden (für Maven sind das alle Testklassen deren Namen u.a. mit „Test“ beginnen oder enden).

Es ist sinnvoll, die Ausführung von Unit- und Integrationstests zu trennen, da Integrationstests üblicherweise länger laufen als Unit-Tests. So kann man die schnelleren Unit-Tests auch einfach mal separat starten, oder die Ausführung der Integrationstests einem CI-Server überlassen.

Damit das maven-failsafe-plugin aktiv wird, muss man es zunächst in der pom.xml konfigurieren. Zudem konfiguriert man das spring-boot-maven-plugin, damit die Anwendung in ein „Fat Jar“ gepackt wird, das mit java -jar ausgefürht werden kann. Der <build>-Abschnitt in der POM sieht dann so aus:

 [...]
  <build>
    <plugins>
      <!-- Spring Boot Fat Jar bauen -->
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <version>${spring.boot.version}</version>
        <executions>
          <execution>
          <goals>
            <goal>repackage</goal>
          </goals>
          </execution>
        </executions>
      </plugin>
      <!-- Integrationstests -->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-failsafe-plugin</artifactId>
        <version>${maven.failsafe.version}</version>
        <executions>
          <execution>
            <goals>
              <goal>integration-test</goal>
              <goal>verify</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
  [...]

Der <executions>-Teil der spring-boot-maven-Konfiguration kann weggelassen werden, wenn die POM von spring-boot-starter-parent erbt. In den Projekten eines unserer Kunden gibt es jedoch eine unternehmensspezifische POM, von der alle Projekte erben. Daher importieren wir die SpringBoot-POM via <dependencyManagement> mit <scope>import</scope> und <type>pom</type>. In unserem Fall muss das repackage-Goal deswegen explizit konfiguriert werden.

Starten wir nun unseren @SpringBootTest MyApplicationIT, kommt es zu einem der beiden folgenden Fehler; entweder:

java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test

oder:

Caused by: java.lang.ClassNotFoundException: de.doubleslash.someapp.MyApplicationClass

Letzterer tritt auf, falls die Testklasse Anwendungsklassen wie hier MyApplicationClass importiert.

Aber woran liegt das? Und wie kann das Problem gelöst werden?

Teil Eins des Problems ist das vom spring-boot-maven-plugin durchgeführte repackage. Dies findet in der package-Phase statt, die den Integrationstests vorgelagert ist. Hier wird das Fat-Jar gebaut, das die Anwendungsklassen und alle Dependencies enthält, und selbstständig gestartet werden kann. Es erhält den Namen des build-Artefakts (z.B. my-app-1.0.jar, das normalerweise (also ohne das spring-boot-maven-plugin) ein normales Jar wäre, das nur die Anwendungsklassen enthält. Das Standard-Jar bleibt erhalten, erhält aber den suffix „.original“ (z.B. my-app-1.0.jar.original).

Teil Zwei des Problems ist dem maven-failsafe-plugin geschuldet, das standardmäßig das zuvor gebaute Jar in den Klassenpfad für die Tests aufnimmt. In diesem Fall das Fat Jar von Spring Boot. Da dieses intern anders organisiert ist als Standard-Jar-Dateien (die Anwendungsklassen liegen im Fat Jar unterhalb von /BOOT-INF/classes/), werden die Klassen nicht gefunden, was in einer der beiden o.g. Exceptions resultiert.

Das Problem stellt sich zudem nur, wenn das Projekt nicht von spring-boot-starter-parent erbt. Schauen wir uns die Parent-POM von Spring Boot an, finden wir den Grund dafür: sie enthält im <pluginManagement> die folgende Konfiguration des failsafe-Plugins:

<pluginManagement>
  [...]
  <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <executions>
      <execution>
        <goals>
          <goal>integration-test</goal>
          <goal>verify</goal>
        </goals>
      </execution>
    </executions>
    <configuration>
      <classesDirectory>${project.build.outputDirectory}</classesDirectory>
    </configuration>
  </plugin>
  [...]
<pluginManagement>

Der Schlüssel zur Lösung ist die Konfiguration von <classesDirectory>: Hier wird dem Test-Klassenpfad das outputDirectory des Builds hinzugefügt, namentlich das Verzeichnis target/classes/. Dadurch werden die Anwendungsklassen für das failsafe-Plugin wieder sichtbar – wenn man von spring-boot-starter-parent erbt.

Leider können PluginManagement-Konfigurationen in Maven nur geerbt werden, wenn man die POM mit der entsprechenden Konfiguration als <parent> (direkt oder indirekt) einbindet. Über den POM-Import können nur DependencyManagement-Konfigurationen „geerbt“ werden, aber keine Plugin-Konfigurationen.

Daher müssen wir bei anderer parent-pom die oben gezeigte <classesDirectory>-Konfiguration in unsere eigene pom.xml übernehmen, damit unser Integrationstest funktioniert.

Eine andere Möglichkeit, das Problem zu lösen, wäre, das Repackage von Spring Boot in eine Phase nach den Integrationstests zu verschieben:

 [...]
  <build>
    <plugins>
      <!-- Spring Boot Fat Jar bauen -->
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <version>${spring.boot.version}</version>
        <executions>
          <execution>
          <goals>
            <goal>repackage</goal>
          </goals>
          <phase>post-integration-test</phase>
          </execution>
        </executions>
      </plugin>
      [...]
    </plugins>
  </build>
  [...]

Nun passiert das Repackage erst nach den Integrationstests (Phase post-integration-tests). Failsafe funktioniert wie vorgesehen, da die Tests mit dem Standard-Jar ausgeführt werden. Sofern die Tatsache nicht stört dass das Erstellen des eigentlichen Build-Artefakts, also des Fat Jars, nicht in der dafür eigentlich vorgesehenen Phase package stattfindet, kann dies auch eine Möglichkeit sein.

Eine weitere Option die Abhilfe schaffen kann ist die Konfiguration eines Classifiers:

 [...]
  <build>
    <plugins>
      <!-- Spring Boot Fat Jar bauen -->
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <version>${spring.boot.version}</version>
        <executions>
          <execution>
          <goals>
            <goal>repackage</goal>
          </goals>
          <configuration>
            <classifier>exec</classifier>
          </configuration>          
          </execution>
        </executions>
      </plugin>
      [...]
    </plugins>
  </build>
  [...]

Damit bleibt das Original-Jar erhalten, und der Name des Fat-Jar wird um den Classifier ergänzt (z.B. my-app-1.0-exec.jar).

Welche der Lösungsmöglichkeiten man wählt ist wohl Geschmackssache. Im Zweifel bleibt man bei Nr. 1, unter der Annahme, dass sich die SpringBoot-Entwickler bestimmt ein paar Gedanken zu ihrer Lösung gemacht haben :-)

Zurück zur Übersicht

2 Kommentare zu “Die Krux mit Spring Boot und dem maven-failsafe-plugin

  1. Danke für den Artikel. Ich habe in unseren Parent eingefügt und die Tests laufen wieder.

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

*Pflichtfelder

*