skip to content

How would you start an external resource before integration tests and guarantee it is torn down afterward using Maven phases?

level: seniorimportance: should knowfreq 45%

answer

  1. start on pre-IT, stop on post-IT
  2. deferred fail -> teardown guaranteed
  3. build-helper reserve-network-port
  4. systemPropertyVariables pass port
  5. Testcontainers as in-JVM alternative

basics

~10 s

Bind a start goal to pre-integration-test and a stop goal to post-integration-test. Failsafe runs tests in integration-test without failing, so post-integration-test teardown always executes before verify judges results.

solid answer

~40 s

I bind the resource's lifecycle to Maven phases around Failsafe. The startup goal (e.g. an embedded Jetty/Tomcat start, a Docker/Testcontainers spin-up, or a DB migration) goes on `pre-integration-test`; the shutdown goal goes on `post-integration-test`. Failsafe's `integration-test` goal runs the tests there but does NOT fail the build, so Maven reaches `post-integration-test` and tears everything down regardless of test outcome. The `verify` goal then fails the build if any IT failed. The key invariant: never let a goal throw between start and teardown, or cleanup is skipped. For port conflicts I use the `build-helper-maven-plugin` `reserve-network-port` goal on `pre-integration-test` to pick a free port and pass it to both the server and the tests via a system property.

code

xml · 10 lines
xml
<execution>
  <id>start-jetty</id>
  <phase>pre-integration-test</phase>
  <goals><goal>start</goal></goals>
</execution>
<execution>
  <id>stop-jetty</id>
  <phase>post-integration-test</phase>
  <goals><goal>stop</goal></goals>
</execution>

go deeper

for a junior

Knows resources start before and stop after the tests.

for a middle

Maps start/stop to pre- and post-integration-test phases.

for a senior

Adds dynamic ports, property injection, and reasons about the no-throw invariant.

for a principal

Chooses between Maven-phase vs in-JVM lifecycles per architecture and standardizes it.

## The phase choreography Failsafe is designed to sit inside a four-phase window: 1. `pre-integration-test` — **start** external resources. 2. `integration-test` — Failsafe `integration-test` goal runs tests (no fail). 3. `post-integration-test` — **stop** external resources. 4. `verify` — Failsafe `verify` goal adjudicates and may fail. Because the test goal defers failure, Maven always progresses to `post-integration-test`, so teardown is guaranteed. ## Example: embedded server start/stop Many server plugins expose start/stop goals. Bind them to the bracket phases: ```xml <plugin> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-maven-plugin</artifactId> <configuration><stopPort>9966</stopPort><stopKey>STOP</stopKey></configuration> <executions> <execution> <id>start-jetty</id> <phase>pre-integration-test</phase> <goals><goal>start</goal></goals> <configuration><scanIntervalSeconds>0</scanIntervalSeconds><daemon>true</daemon></configuration> </execution> <execution> <id>stop-jetty</id> <phase>post-integration-test</phase> <goals><goal>stop</goal></goals> </execution> </executions> </plugin> ``` ## Dynamic ports Hardcoded ports collide on CI agents. Reserve one dynamically: ```xml <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <executions> <execution> <id>reserve-port</id> <phase>pre-integration-test</phase> <goals><goal>reserve-network-port</goal></goals> <configuration><portNames><portName>it.http.port</portName></portNames></configuration> </execution> </executions> </plugin> ``` Then pass `${it.http.port}` to both the server config and to Failsafe via `<systemPropertyVariables>` so the tests know where to connect. ## Modern alternative: in-test lifecycle Testcontainers/Spring Boot often manage the resource **inside** the test JVM (e.g. `@Testcontainers`, `@SpringBootTest(webEnvironment=RANDOM_PORT)`), making Maven phase choreography unnecessary. But the Maven-phase approach still matters for resources that must exist for the whole IT suite or that the JVM can't own (external services, shared fixtures). Even then, Failsafe's deferred failure is what makes teardown reliable. ## The cardinal rule Do not bind any goal that can throw between `pre-integration-test` and `post-integration-test`. If it throws, Maven aborts before teardown. That's exactly why Failsafe itself refuses to throw in the `integration-test` phase.

  • How do you avoid port collisions on shared CI runners?
    Use build-helper-maven-plugin's reserve-network-port on pre-integration-test to allocate a free port, then inject it into the server and into Failsafe via systemPropertyVariables.
  • When can you skip the Maven phase choreography entirely?
    When the resource lives inside the test JVM (Testcontainers, Spring Boot embedded server with random port); the test framework handles start/stop and Failsafe just runs the tests.

saying these in an interview costs you the question

  • Binding the teardown to verify instead of post-integration-test.
  • Hardcoding ports that collide on CI.
  • Putting a throwing goal between start and teardown, defeating the guarantee.

context