Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Тестирование

Код Jakarta EE работает внутри сервера: внедрение, транзакции и REST даёт контейнер. Поэтому тесты делятся на два вида. Модульные проверяют логику класса без сервера — быстро, за доли секунды. Интеграционные запускают настоящий сервер и обращаются к приложению по HTTP — медленнее, зато проверяют всё вместе.

Подходы

ПодходЧто проверяетСкорость
JUnit без сервераЛогику бинов: зависимости подставляются вручнуюМиллисекунды
JUnit + HTTP к запущенному серверуAPI целиком, как его видит клиентСекунды
Тесты в режиме liberty:devТо же, запуск по Enter прямо во время разработкиСекунды
ArquillianТест разворачивается внутрь сервера, в нём работает @InjectСекунды
TestcontainersСервер и база в Docker-контейнерахДесятки секунд

Зависимости

В pom.xml добавьте JUnit и плагин Failsafe, который запускает интеграционные тесты:

<!-- pom.xml (фрагмент) -->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>6.1.3</version>
    <scope>test</scope>
</dependency>
<!-- pom.xml (фрагмент, внутри 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>

Модульный тест

Бин — обычный класс, поэтому его можно создать через new и подставить зависимости самому. Поля с @Inject у нас доступны в пределах пакета, а тест лежит в том же пакете:

// 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("Первая", "Текст первой записи"),
                new NewPost("Вторая", "Текст второй записи")));
 
        assertEquals(2, count);
        assertEquals("2026-10-04", saved.get(0).getCreatedAt().toString());
    }
 
    @Test
    void rejectsPostWithoutTitle() {
        assertThrows(IllegalArgumentException.class,
                () -> service.importPosts(List.of(new NewPost("", "Без заголовка"))));
    }
}

Вместо базы — анонимный подкласс репозитория, который складывает записи в список, вместо системных часов — Clock.fixed. Здесь окупился продюсер часов из главы про CDI.

Интеграционный тест

Класс с окончанием IT запускает Failsafe. Тест отправляет настоящие HTTP-запросы через 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("Привет, Jakarta EE"));
    }
 
    @Test
    void createsPost() throws Exception {
        HttpRequest request = HttpRequest.newBuilder(URI.create(BASE))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(
                        "{\"title\":\"Из теста\",\"body\":\"Создано в 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());
    }
}

Чтобы сервер поднимался перед тестами и останавливался после, добавьте плагину Liberty выполнения в фазах Maven:

<!-- pom.xml (фрагмент, внутри 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>

Запуск

./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

Вывод сокращён. Модульные тесты идут в фазе test, затем Maven собирает WAR, запускает сервер, выполняет *IT и останавливает сервер. В режиме liberty:dev те же тесты запускаются нажатием Enter без перезапуска сервера.

Частая ошибка

Запустить интеграционные тесты без работающего сервера, например командой ./mvnw failsafe:integration-test в обход фаз. Все тесты падают одинаково:

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

ConnectException значит, что на порту 8080 никто не слушает. Запускайте ./mvnw verify или держите открытым liberty:dev.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.