ZetCode

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:

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.

greeter/src/main/java/com/example/greeter/Greeting.java
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.

greeter/src/main/java/com/example/greeter/GreetingServer.java
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.

greeter/pom.xml
<?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.

greeter/src/test/java/com/example/greeter/GreetingTest.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.

greeter/src/test/java/com/example/greeter/GreetingServerIT.java
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.

pom.xml (Antrun plugin)
<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.

src/test/java/com/example/greeter/ContentTypeIT.java
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

Source

Maven Failsafe Plugin

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Java tutorials.