Skip to content

PATTERN Cited by 1 source

Failsafe integration test separation

Pattern

Split Maven's test run into two phases with two different plugins:

  • Surefire owns the test phase and runs unit tests by naming convention (*Test / *Tests / *TestCase).
  • Failsafe owns the integration-test + verify phases and runs integration tests (*IntegrationTest).

Gate Failsafe behind a Maven profile (with-integration-tests is the Zalando ZMS name) so:

  • mvn test — fast, Surefire only, seconds. Developer inner loop, laptop-friendly.
  • mvn verify -P with-integration-tests — slower, Failsafe runs the IT suite. CI, demand-driven local runs, pre-commit gates.

Why separate phases

  • Phase semantics matter. Unit-test failures should fail fast in test. IT failures are expensive (real containers, DB state) and shouldn't block the faster feedback loop by default.
  • Different plugin behaviour. Failsafe is explicitly designed to always run verify even after IT failures, giving post-test cleanup hooks a chance to run (important when shared containers are in play — see patterns/shared-static-container-across-tests). Surefire just aborts.
  • Naming convention enforces the discipline. Anyone adding an IT that accidentally lands in Surefire (e.g. named FooTest instead of FooIntegrationTest) slows the dev inner loop for everyone. Convention + plugin include-filters is the cheapest enforcement.

Mechanism

Minimum ZMS-style pom.xml fragment (sources/2021-02-24-zalando-integration-tests-with-testcontainers):

<profiles>
  <profile>
    <id>with-integration-tests</id>
    <build>
      <pluginManagement>
        <plugins>
          <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>
              <includes>
                <include>**/*IntegrationTest.java</include>
              </includes>
            </configuration>
          </plugin>
        </plugins>
      </pluginManagement>
    </build>
  </profile>
</profiles>

CI invokes: mvn clean verify -P with-integration-tests.

Plain mvn test skips Failsafe entirely.

Variants

  • Always-on Failsafe, skip-via-property instead of a profile: <skipITs> / -DskipITs=true. Equivalent semantics; profile is more discoverable.
  • Tag-based inclusion via JUnit 5 @Tag("integration") + Surefire <groups> / <excludedGroups>. Reduces the need for two plugins but loses the phase-separation semantics.
  • Separate Gradle source sets (src/integrationTest/java) for Gradle equivalents. Same pattern, different build tool.

Tradeoffs

  • Second plugin to maintain. Version bumps and configuration for both.
  • Naming-convention drift. Test classes that look unit-y but hit real resources slip through if the convention isn't policed. Code review + lint rules mitigate.
  • CI config multiplies. CI pipelines must invoke the right Maven targets; regression in CI config (dropping -P with-integration-tests) silently disables the IT suite.

Seen in

Last updated · 476 distilled / 1,218 read