Encodera

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

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

Оглавление

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

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

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