Тестирование с @MicronautTest
Проверять API вручную через curl после каждого изменения долго. Тесты делают это автоматически. Аннотация @MicronautTest запускает для тестового класса настоящее приложение: контекст с бинами, базу H2 и HTTP-сервер на случайном порту. В тест можно внедрить любой бин через @Inject, как в обычный класс.
Где лежат тесты
Тесты живут в src/test/java/com/example/. Генератор уже создал BlogTest.java — он проверяет, что приложение запускается. Новый тест можно создать вручную или командой mn create-test ArticleController. Запуск всех тестов:
./mvnw testТест контроллера
// src/test/java/com/example/ArticleControllerTest.java
package com.example;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@MicronautTest
class ArticleControllerTest {
@Inject
@Client("/")
HttpClient client;
@Test
void createsAndReadsArticle() {
HttpResponse<Article> response = client.toBlocking().exchange(
HttpRequest.POST("/articles", new ArticleForm("Тестирование", "Текст")),
Article.class);
assertEquals(HttpStatus.CREATED, response.getStatus());
Long id = response.body().id();
Article loaded = client.toBlocking().retrieve("/articles/" + id, Article.class);
assertEquals("Тестирование", loaded.title());
}
@Test
void returns404ForUnknownArticle() {
HttpClientResponseException e = assertThrows(HttpClientResponseException.class,
() -> client.toBlocking().retrieve("/articles/999", Article.class));
assertEquals(HttpStatus.NOT_FOUND, e.getStatus());
}
}@Client("/") даёт HTTP-клиент, настроенный на тестовый сервер. exchange возвращает весь ответ со статусом и телом, retrieve — только тело. Ответ с кодом 400 и выше клиент выбрасывает как HttpClientResponseException, поэтому ошибки проверяют через assertThrows. Третий тест в этом классе, rejectsBlankTitle, так же отправляет статью с пустым заголовком и ждёт HttpStatus.BAD_REQUEST.
Подмена бина
Тест не должен ходить в интернет. Метод с @MockBean заменяет настоящий бин на ваш на время теста:
// src/test/java/com/example/ImportControllerTest.java
@MicronautTest
class ImportControllerTest {
@Inject
@Client("/")
HttpClient client;
@MockBean(PlaceholderClient.class)
PlaceholderClient fakePlaceholder() {
return new PlaceholderClient() {
@Override
public Optional<ExternalPost> fetchPost(Long id) {
return Optional.of(new ExternalPost(id, 1L, "Поддельный заголовок", "Текст"));
}
@Override
public List<ExternalPost> fetchByUser(Long userId) {
return List.of();
}
};
}
@Test
void importsPostFromRemoteServer() {
Article article = client.toBlocking().retrieve(
HttpRequest.POST("/import/5", ""), Article.class);
assertEquals("Поддельный заголовок", article.title());
}
}ImportController получит поддельный клиент вместо настоящего. Вместо анонимного класса можно взять Mockito (возможность mockito в генераторе).
Результат
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0 -- in com.example.ArticleControllerTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 -- in com.example.ArticleServiceTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 -- in com.example.BlogTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 -- in com.example.ImportControllerTest
[INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESSЧастая ошибка
По умолчанию @MicronautTest оборачивает каждый тестовый метод в транзакцию и откатывает её в конце, чтобы тесты не мешали друг другу. Но тогда метод publish с @Transactional присоединяется к транзакции теста и не откатывается сам. Вот тест, который проверяет откат:
// src/test/java/com/example/ArticleServiceTest.java
@MicronautTest
class ArticleServiceTest {
@Inject
ArticleService articleService;
@Inject
ArticleRepository articleRepository;
@Test
void publishRollsBackWhenCommentIsBlank() {
long before = articleRepository.count();
assertThrows(IllegalArgumentException.class,
() -> articleService.publish(new ArticleForm("Черновик", "Текст"), " "));
assertEquals(before, articleRepository.count());
}
}С обычной @MicronautTest он падает: внутри общей транзакции статья ещё видна.
[ERROR] ArticleServiceTest.publishRollsBackWhenCommentIsBlank:26 expected: <0> but was: <1>Для тестов, которые проверяют сами транзакции, отключите обёртку: @MicronautTest(transactional = false).