Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Testing

Jakarta EE code runs inside a server: the container provides injection, transactions and REST. So tests come in two kinds. Unit tests check the logic of a class without a server — fast, in fractions of a second. Integration tests start a real server and talk to the application over HTTP — slower, but they check everything together.

Approaches

ApproachWhat it checksSpeed
JUnit without a serverBean logic: dependencies are set by handMilliseconds
JUnit + HTTP to a running serverThe whole API as a client sees itSeconds
Tests in liberty:dev modeThe same, run with Enter right during developmentSeconds
ArquillianThe test is deployed inside the server, @Inject works in itSeconds
TestcontainersServer and database in Docker containersTens of seconds

Dependencies

Add JUnit and the Failsafe plugin, which runs integration tests, to pom.xml:

<!-- pom.xml (fragment) -->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>6.1.3</version>
    <scope>test</scope>
</dependency>
<!-- pom.xml (fragment, inside build/plugins) -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>3.6.0</version>
    <executions>
        <execution>
            <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
            </goals>
        </execution>
    </executions>
</plugin>

A unit test

A bean is an ordinary class, so you can create it with new and set its dependencies yourself. Our @Inject fields are package-private, and the test lives in the same package:

// src/test/java/com/example/blog/PostServiceTest.java
package com.example.blog;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
 
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
 
class PostServiceTest {
 
    private final List<Post> saved = new ArrayList<>();
    private PostService service;
 
    @BeforeEach
    void setUp() {
        service = new PostService();
        service.clock = Clock.fixed(Instant.parse("2026-10-04T10:00:00Z"), ZoneOffset.UTC);
        service.repository = new PostRepository() {
            @Override
            public Post save(Post post) {
                saved.add(post);
                return post;
            }
        };
    }
 
    @Test
    void importsAllPosts() {
        int count = service.importPosts(List.of(
                new NewPost("First", "Body of the first post"),
                new NewPost("Second", "Body of the second post")));
 
        assertEquals(2, count);
        assertEquals("2026-10-04", saved.get(0).getCreatedAt().toString());
    }
 
    @Test
    void rejectsPostWithoutTitle() {
        assertThrows(IllegalArgumentException.class,
                () -> service.importPosts(List.of(new NewPost("", "No title"))));
    }
}

Instead of a database there is an anonymous repository subclass that collects posts into a list, and instead of the system clock there is Clock.fixed. This is where the clock producer from the CDI chapter pays off.

An integration test

Failsafe runs classes whose names end in IT. The test sends real HTTP requests through java.net.http.HttpClient:

// src/test/java/com/example/blog/PostResourceIT.java
package com.example.blog;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
 
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import org.junit.jupiter.api.Test;
 
class PostResourceIT {
 
    private static final String BASE = "http://localhost:8080/blog/api/posts";
    private final HttpClient client = HttpClient.newHttpClient();
 
    @Test
    void listsPosts() throws Exception {
        HttpRequest request = HttpRequest.newBuilder(URI.create(BASE)).GET().build();
 
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
 
        assertEquals(200, response.statusCode());
        assertTrue(response.body().contains("Hello, Jakarta EE"));
    }
 
    @Test
    void createsPost() throws Exception {
        HttpRequest request = HttpRequest.newBuilder(URI.create(BASE))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(
                        "{\"title\":\"From a test\",\"body\":\"Created by PostResourceIT\"}"))
                .build();
 
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
 
        assertEquals(201, response.statusCode());
        assertTrue(response.headers().firstValue("Location").isPresent());
    }
 
    @Test
    void returns404ForMissingPost() throws Exception {
        HttpRequest request = HttpRequest.newBuilder(URI.create(BASE + "/999")).GET().build();
 
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
 
        assertEquals(404, response.statusCode());
    }
}

To start the server before the tests and stop it afterwards, add executions bound to Maven phases to the Liberty plugin:

<!-- pom.xml (fragment, inside liberty-maven-plugin) -->
<executions>
    <execution>
        <id>start-server</id>
        <phase>pre-integration-test</phase>
        <goals>
            <goal>create</goal>
            <goal>install-feature</goal>
            <goal>deploy</goal>
            <goal>start</goal>
        </goals>
    </execution>
    <execution>
        <id>stop-server</id>
        <phase>post-integration-test</phase>
        <goals>
            <goal>stop</goal>
        </goals>
    </execution>
</executions>

Running

./mvnw verify
[INFO] Running com.example.blog.PostServiceTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO] --- liberty:3.12.3:start (start-server) @ blog ---
[INFO] Running com.example.blog.PostResourceIT
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO] --- liberty:3.12.3:stop (stop-server) @ blog ---
[INFO] BUILD SUCCESS

The output is shortened. Unit tests run in the test phase, then Maven builds the WAR, starts the server, runs the *IT classes and stops the server. In liberty:dev mode the same tests run when you press Enter, without restarting the server.

Common mistake

Running integration tests without a running server, for example with ./mvnw failsafe:integration-test, which skips the phases. All tests fail the same way:

[ERROR] Tests run: 3, Failures: 0, Errors: 3, Skipped: 0 <<< FAILURE! -- in com.example.blog.PostResourceIT
java.net.ConnectException

ConnectException means nothing is listening on port 8080. Run ./mvnw verify or keep liberty:dev open.

Thanks for reading!

Donate to the site — we work hard for you.