Gymterview
middle

Как работает Transactional в тестах и что такое автоматический rollback?

Когда тестовый метод или класс аннотирован @Transactional, Spring Test автоматически оборачивает каждый тест в транзакцию и откатывает её после завершения. Это гарантирует, что тестовые данные не загрязняют базу для последующих тестов.

Аналогия из жизни: это как черновик в текстовом редакторе — вы пишете, проверяете результат, а потом нажимаете «отменить» (Ctrl+Z), и документ возвращается в исходное состояние.

Пример
@SpringBootTest
@Transactional
class UserServiceTransactionalTest {

    @Autowired
    private UserService userService;

    @Autowired
    private UserRepository userRepository;

    @Test
    void shouldCreateUserAndRollback() {
        userService.create(new UserDto("Иван", "ivan@example.com"));
        assertEquals(1, userRepository.count());
        // После теста транзакция откатится — данные не сохранятся
    }

    @Test
    void shouldHaveEmptyDatabase() {
        // Благодаря rollback предыдущего теста, база чиста
        assertEquals(0, userRepository.count());
    }
}

Отключение отката

Пример
@Test
@Commit  // или @Rollback(false)
void shouldPersistData() {
    userService.create(new UserDto("Иван", "ivan@example.com"));
    // Данные останутся в базе после теста
}

Важные нюансы

  1. @DataJpaTest автоматически применяет @Transactional — дополнительно аннотировать не нужно.

  2. @Transactional в тестах не работает с RANDOM_PORT и DEFINED_PORT, потому что HTTP-запросы выполняются в отдельном потоке, а транзакция привязана к потоку теста:

Пример
// Rollback НЕ сработает — запросы идут через реальный HTTP
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Transactional
class WontRollbackTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void dataWillPersist() {
        restTemplate.postForEntity("/api/users",
            new UserDto("Иван"), UserDto.class);
        // Данные останутся в базе!
    }
}
  1. Для очистки данных при интеграционных тестах с реальным сервером используйте @Sql с AFTER_TEST_METHOD или @DirtiesContext:
Пример
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class IntegrationTest {

    @Test
    @Sql(scripts = "/cleanup.sql",
         executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    void integrationTest() {
        // ...
    }
}

На собеседовании: ключевой момент — объяснить, почему rollback не работает с RANDOM_PORT (разные потоки). Частая ошибка — не знать про эту особенность и ожидать откат при использовании TestRestTemplate.