Encodera

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

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

Оглавление

Тесты с @SpringBootTest

В учебнике вы писали тесты JUnit для обычных классов. Сервис блога так не проверить: ему нужны репозитории, база и Clock. Аннотация @SpringBootTest поднимает в тесте весь контейнер Spring — так же, как при запуске, только без открытия порта. Бины можно получить прямо в тестовый класс.

Что уже есть

Генератор положил в проект первый тест, а в pom.xml — тестовые стартеры (spring-boot-starter-webmvc-test и другие с -test), в которых есть JUnit 5, AssertJ и Mockito.

// src/test/java/com/example/blog/BlogApplicationTests.java
@SpringBootTest
@ActiveProfiles("test")
class BlogApplicationTests {
 
    @Test
    void contextLoads() {
    }
}

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

Своя база для тестов

Тесты не должны трогать файл data/blog.mv.db. Создадим профиль test с базой в памяти:

# src/test/resources/application-test.yaml
spring:
  datasource:
    url: jdbc:h2:mem:blog-test
  jpa:
    show-sql: false

@ActiveProfiles("test") включает этот профиль. База mem: живёт, пока работает тестовый процесс; Flyway создаст в ней таблицы при старте.

Тест сервиса

// src/test/java/com/example/blog/PostServiceTests.java
package com.example.blog;
 
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;
 
@SpringBootTest
@ActiveProfiles("test")
@Transactional
class PostServiceTests {
 
    @Autowired
    private PostService postService;
 
    @Autowired
    private UserRepository users;
 
    @Test
    void createsPostWithAuthor() {
        users.save(new User("anna", "{noop}changeme"));
 
        PostResponse post = postService.create(new NewPostRequest("Привет", "Текст"), "anna");
 
        assertThat(post.id()).isNotNull();
        assertThat(post.author()).isEqualTo("anna");
    }
 
    @Test
    void throwsWhenPostIsMissing() {
        assertThatThrownBy(() -> postService.findById(999L))
                .isInstanceOf(PostNotFoundException.class)
                .hasMessage("Статья 999 не найдена");
    }
}

В тестах бины получают через @Autowired на поле — конструктор тестового класса вызывает JUnit, а не Spring. {noop} — приставка «пароль без шифрования», для тестовых данных это допустимо. @Transactional над тестовым классом откатывает транзакцию после каждого теста, и база снова пуста.

Запуск

./mvnw test
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.049 s -- in com.example.blog.PostServiceTests
[INFO] Tests run: 12, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

Первый тест в прогоне идёт секунд десять — Spring поднимает приложение. Остальные классы с теми же настройками используют уже готовый контейнер, поэтому быстры. Команда ./mvnw package тоже запускает тесты и не соберёт JAR, если они упали.

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

Без @Transactional данные одного теста остаются в базе для следующих. Тест, который в @BeforeEach создаёт Анну, упадёт со второго раза:

[ERROR] Tests run: 4, Failures: 0, Errors: 3, Skipped: 0 <<< FAILURE! -- in com.example.blog.PostControllerTests
Unique index or primary key violation: "PUBLIC.CONSTRAINT_4D INDEX PUBLIC.CONSTRAINT_4D_INDEX_6 ON PUBLIC.USERS(USERNAME NULLS FIRST) VALUES ( /* 1 */ 'anna' )"

Пользователь anna уже есть, а имя уникально. Добавьте @Transactional к тестовому классу — и каждый тест начнётся с чистой базы.

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

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