Maven Failsafe Plugin
last modified November 13, 2026
In this article we run integration tests with the Maven Failsafe Plugin. We write an integration test for a small HTTP server, look at the phases in which Failsafe works, and select and skip integration tests. The examples use Maven 3.10, Failsafe 3.6.1, JUnit 6, and Java 25.
Unit tests check classes in isolation and are run by the
Surefire plugin in the
test phase, before the application is packaged. Integration
tests check how parts of the application work together and with the
outside world: a running server, a database, or a file system. They run
after packaging, against the packaged artifact. Failsafe is a fork of
Surefire made for this purpose; the two plugins share most of their
parameters and the test providers.
The phases
The default lifecycle has four phases for integration tests, which come
after package:
pre-integration-testprepares the environment, for example starts a server or a container,integration-testruns the tests,post-integration-testtears down the environment,verifychecks the results.
Failsafe has two goals. The integration-test goal runs the
tests but never fails the build; it only records the results. The
verify goal fails the build if any test failed. This split
ensures that post-integration-test always runs and cleans up
the environment, even when the tests fail. For that reason, integration
tests are run with mvn verify, not with
mvn integration-test.
The project
Our application is a small HTTP server that greets users. It uses the HTTP server built into the JDK.
package com.example.greeter;
public class Greeting {
public static String of(String name) {
if (name == null || name.isBlank()) {
return "Hello, stranger!";
}
return "Hello, " + name.strip() + "!";
}
}
The Greeting class creates the greeting. It is covered by a
unit test.
package com.example.greeter;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
public class GreetingServer {
private final HttpServer server;
public GreetingServer(int port) throws IOException {
server = HttpServer.create(new InetSocketAddress("localhost", port), 0);
server.createContext("/hello", exchange -> {
String query = exchange.getRequestURI().getRawQuery();
String name = query != null && query.startsWith("name=")
? URLDecoder.decode(query.substring(5), StandardCharsets.UTF_8)
: null;
byte[] body = Greeting.of(name).getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");
exchange.sendResponseHeaders(200, body.length);
try (var os = exchange.getResponseBody()) {
os.write(body);
}
});
}
public int start() {
server.start();
return server.getAddress().getPort();
}
public void stop() {
server.stop(0);
}
public static void main(String[] args) throws IOException {
int port = new GreetingServer(8080).start();
System.out.println("listening on port " + port);
}
}
The server answers requests to the /hello path. The port is
passed to the constructor; port 0 lets the operating system choose a free
port, which the start method returns.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>greeter</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<maven.compiler.release>25</maven.compiler.release>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>6.1.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.6.1</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Unlike Surefire, Failsafe is not bound to the lifecycle by default. We must add an execution with its two goals; both goals bind to their phases automatically.
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
The execution has no ID, so Maven names it default. The
version of Failsafe should match the version of Surefire.
The tests
Failsafe runs test classes whose names match **/IT*.java,
**/*IT.java, or **/*ITCase.java. Surefire ignores
these classes, so both kinds of tests can live in
src/test/java.
package com.example.greeter;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class GreetingTest {
@Test
void greetsByName() {
assertEquals("Hello, Lucy!", Greeting.of(" Lucy "));
}
@Test
void greetsStranger() {
assertEquals("Hello, stranger!", Greeting.of(""));
}
}
The unit test checks the Greeting class without a server.
package com.example.greeter;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import static org.junit.jupiter.api.Assertions.assertEquals;
class GreetingServerIT {
private static GreetingServer server;
private static URI base;
private static final HttpClient client = HttpClient.newHttpClient();
@BeforeAll
static void startServer() throws Exception {
server = new GreetingServer(0);
int port = server.start();
base = URI.create("http://localhost:" + port);
System.out.println("server class from " + GreetingServer.class
.getProtectionDomain().getCodeSource().getLocation());
}
@AfterAll
static void stopServer() {
server.stop();
}
private HttpResponse<String> get(String path) throws Exception {
var request = HttpRequest.newBuilder(base.resolve(path)).build();
return client.send(request, HttpResponse.BodyHandlers.ofString());
}
@Test
void greetsByName() throws Exception {
var response = get("/hello?name=Lucy%20Smith");
assertEquals(200, response.statusCode());
assertEquals("Hello, Lucy Smith!", response.body());
}
@Test
void greetsStranger() throws Exception {
assertEquals("Hello, stranger!", get("/hello").body());
}
}
The integration test starts the server on a free port before the tests,
sends real HTTP requests with the JDK HTTP client, and stops the server at
the end. It also prints the location from which the
GreetingServer class was loaded.
Running integration tests
We run the build up to the verify phase.
Surefire runs the unit test before packaging; the JAR plugin builds the
JAR; then Failsafe runs the integration test, and its
verify goal checks the results. The printed location shows
that the integration test uses the packaged JAR, not the
target/classes directory. For the jar packaging,
Failsafe puts the artifact on the test classpath, so the tests also check
the packaging, for example resources that were filtered or left out.
Failsafe writes its reports into the target/failsafe-reports
directory; the failsafe-summary.xml file is the record that
the verify goal reads.
$ mvn verify --- surefire:3.6.1:test (default-test) @ greeter --- Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.048 s -- in com.example.greeter.GreetingTest Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 --- jar:3.5.1:jar (default-jar) @ greeter --- --- failsafe:3.6.1:integration-test (default) @ greeter --- server class from file:/home/user/project/greeter/target/greeter-1.0.0-SNAPSHOT.jar Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.282 s -- in com.example.greeter.GreetingServerIT Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 --- failsafe:3.6.1:verify (default) @ greeter --- BUILD SUCCESS $ ls target/failsafe-reports com.example.greeter.GreetingServerIT.txt failsafe-summary.xml TEST-com.example.greeter.GreetingServerIT.xml
Setting up and tearing down
Real integration tests often need an environment that is started before
the tests and stopped after them, such as a database or the application
itself. Plugins that do this, like the Docker or Cargo plugins, bind goals
to the pre-integration-test and
post-integration-test phases. To see the order of the phases,
we add two Antrun executions that only print messages.
<artifactId>maven-antrun-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>start-environment</id>
<phase>pre-integration-test</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<echo message="starting test environment"/>
</target>
</configuration>
</execution>
<execution>
<id>stop-environment</id>
<phase>post-integration-test</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<echo message="stopping test environment"/>
</target>
</configuration>
</execution>
</executions>
The start-environment execution runs in the
pre-integration-test phase and the
stop-environment execution in the
post-integration-test phase.
package com.example.greeter;
import org.junit.jupiter.api.Test;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ContentTypeIT {
@Test
void returnsPlainText() throws Exception {
var server = new GreetingServer(0);
int port = server.start();
try (var client = HttpClient.newHttpClient()) {
var request = HttpRequest.newBuilder(
URI.create("http://localhost:" + port + "/hello")).build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
assertEquals("text/plain", response.headers().firstValue("Content-Type").orElseThrow());
} finally {
server.stop();
}
}
}
We also add an integration test that fails. The server sends the
character set in the Content-Type header, which the test does
not expect.
With mvn verify, the integration test fails, the environment
is stopped, and only then the verify goal fails the build.
With mvn integration-test, the build ends after the tests: the
environment is never stopped and, because the verify goal
does not run, the build succeeds despite the failed test.
$ mvn verify --- antrun:3.2.0:run (start-environment) @ greeter --- [echo] starting test environment --- failsafe:3.6.1:integration-test (default) @ greeter --- Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.240 s <<< FAILURE! -- in com.example.greeter.ContentTypeIT Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.024 s -- in com.example.greeter.GreetingServerIT ContentTypeIT.returnsPlainText:22 expected: <text/plain> but was: <text/plain; charset=utf-8> Tests run: 3, Failures: 1, Errors: 0, Skipped: 0 --- antrun:3.2.0:run (stop-environment) @ greeter --- [echo] stopping test environment --- failsafe:3.6.1:verify (default) @ greeter --- BUILD FAILURE There are test failures. $ mvn integration-test --- antrun:3.2.0:run (start-environment) @ greeter --- [echo] starting test environment --- failsafe:3.6.1:integration-test (default) @ greeter --- Tests run: 3, Failures: 1, Errors: 0, Skipped: 0 BUILD SUCCESS
Selecting and skipping tests
The it.test property selects integration tests in the same way
as the test property of Surefire. The skipITs
property skips the integration tests and keeps the unit tests.
Note the third command. In Failsafe 3.6, the skipTests
parameter is deprecated and no longer bound to the skipTests
property, so -DskipTests skips only the unit tests; Failsafe
runs the integration tests, and the failing test breaks the build. Up to
version 3.5, Failsafe skipped the integration tests too. To skip all tests, we use
-DskipTests -DskipITs, or -Dmaven.test.skip=true,
which also skips compiling the tests.
$ mvn verify -Dit.test=GreetingServerIT --- failsafe:3.6.1:integration-test (default) @ greeter --- Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.271 s -- in com.example.greeter.GreetingServerIT --- failsafe:3.6.1:verify (default) @ greeter --- BUILD SUCCESS $ mvn verify -DskipITs --- surefire:3.6.1:test (default-test) @ greeter --- Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 --- failsafe:3.6.1:integration-test (default) @ greeter --- Tests are skipped. --- failsafe:3.6.1:verify (default) @ greeter --- Tests are skipped. BUILD SUCCESS $ mvn verify -DskipTests --- surefire:3.6.1:test (default-test) @ greeter --- Tests are skipped. --- failsafe:3.6.1:integration-test (default) @ greeter --- Tests run: 3, Failures: 1, Errors: 0, Skipped: 0 --- failsafe:3.6.1:verify (default) @ greeter --- BUILD FAILURE $ mvn verify -Dmaven.test.skip=true --- surefire:3.6.1:test (default-test) @ greeter --- Tests are skipped. --- failsafe:3.6.1:integration-test (default) @ greeter --- Tests are skipped. --- failsafe:3.6.1:verify (default) @ greeter --- Tests are skipped. BUILD SUCCESS
Best practices
- Run integration tests with
mvn verify, so that the environment is always torn down. - Name integration tests with the
ITsuffix and keep them insrc/test/java, or in a separate module when they need a different set of dependencies. - Use free ports and temporary directories, so that integration tests can run in parallel builds on the same machine.
- Keep the Surefire and Failsafe versions in sync, for example through a shared property.
- Skip integration tests with
-DskipITs; do not rely on-DskipTeststo skip them.
Source
In this article we have run integration tests with the Maven Failsafe Plugin, used the phases around them, and selected and skipped integration tests.
Author
List all Java tutorials.