Тестирование
Код 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.