The crux with Spring Boot and the maven-failsafe-plugin

17.07.2020 -

In certain scenarios, @SpringBootTestsand the maven-failsafe-plugin do not get along very well. You can find out the background to this and how you can solve the associated problems here.

 

Anyone who develops applications with Spring Boot usually (or hopefully! ;-) also writes @SpringBootTests. As the Spring ApplicationContext is started for this purpose, these are integration tests.

Maven provides the "integrationtest" phase specifically for this purpose. There, the maven-failsafe-plugin ensures that all test classes whose names begin or end with "IT" are executed. On the other hand, there is the upstream phase "test", in which the unit tests are executed by the maven-surefire-plugin (for Maven, these are all test classes whose names begin or end with "Test", among others).

It makes sense to separate the execution of unit and integration tests, as integration tests usually run longer than unit tests. This means that you can simply start the faster unit tests separately or leave the execution of the integration tests to a CI server.

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.

If we now start our @SpringBootTest MyApplicationIT, one of the following two errors occurs; either:

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

or:

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

The latter occurs if the test class imports application classes such as MyApplicationClass here.

But why is that? And how can the problem be solved?

Part one of the problem is the repackage performed by the spring-boot-maven-plugin. This takes place in the package phase, which precedes the integration tests. This is where the fat jar is built, which contains the application classes and all dependencies and can be started independently. It is given the name of the build artifact (e.g. my-app-1.0.jar, which would normally (i.e. without the spring-boot-maven-plugin) be a normal jar that only contains the application classes. The standard jar is retained, but is given the suffix ".original" (e.g. my-app-1.0.jar.original).

Part two of the problem is due to the maven-failsafe-plugin, which by default includes the previously built jar in the classpath for the tests. In this case, the Spring Boot fat jar. Since this is internally organized differently than standard jar files (the application classes are located in the fat jar below /BOOT-INF/classes/), the classes are not found, which results in one of the two exceptions mentioned above.

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.

Another way to solve the problem would be to move the repackage of Spring Boot to a phase after the integration tests:

 [...]
  <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>
  [...]

Now the repackage only happens after the integration tests ( post-integration-tests phase). Failsafe works as intended, as the tests are executed with the standard jar. Provided that the fact that the creation of the actual build artifact, i.e. the fat jar, does not take place in the package phase actually intended for this, this can also be a possibility.

Another option that can help is the configuration of a classifier:

 [...]
  <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>
  [...]

This retains the original jar, and the name of the fat jar is supplemented by the classifier (e.g. my-app-1.0-exec.jar).

Which of the possible solutions you choose is probably a matter of taste. If in doubt, stick with no. 1, assuming that the SpringBoot developers have certainly put some thought into their solution :-)

Back to overview

2 comments on "The crux with Spring Boot and the maven-failsafe-plugin"

  1. Thanks for the article. I have added it to our parent and the tests are running again.

Write a comment

Your e-mail address will not be published. Required fields are marked with *

*Mandatory fields

*