- Вопрос или проблема
- Ответ или решение
- Проблема с тестированием аннотации @Transactional в рамках @DataJpaTest
- Проблема
- Решение
- 1. Деактивируйте аннотацию @Transactional в тестах
- 2. Импортируйте ваш контроллер
- 3. Используйте конфигурацию теста с поддержкой транзакций
- 4. Свойства для отображения SQL
- Заключение
Вопрос или проблема
Я застрял со своими модульными тестами вокруг метода @Transactional
. Проблема в том, что я не могу протестировать механизм отката внутри @DataJpaTest
.
Вот мой упрощенный пример приложения:
@RestController
public class MyController {
private final MyRepository myRepository;
private final MyService myService;
public MyController(MyRepository myRepository, MyService myService) {
this.myRepository = myRepository;
this.myService = myService;
}
@GetMapping
@Transactional
public String justForTest() {
var myEntity = new MyEntity().setStatus("NEW");
myRepository.save(myEntity);
try {
myService.throwsAnRuntimeException();
// если сервис успешен
myEntity.setStatus("SUCCESS");
} catch (Exception e) {
// если сервис завершился неудачно
myEntity.setStatus("FAIL");
throw e;
} finally {
myRepository.save(myEntity);
}
return "this is the end";
}
}
@Data
@Entity
@Accessors(chain = true)
public class MyEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String status;
}
@Repository
public interface MyRepository extends JpaRepository<MyEntity, Long> {
}
@Service
public class MyService {
public void throwsAnRuntimeException() {
throw new RuntimeException("Откаты не работают!");
}
}
Таким образом, когда я запускаю этот код привычным способом (с локальной базой данных PostgreSQL), он работает, как и ожидалось: когда я вызываю запрос на localhost:8080, ответ – 500, и таблица в моей базе данных полностью пуста, как и ожидалось, потому что, когда метод @Transactional
завершает работу с RuntimeException
, он делает rollback
всей транзакции (даже если в финальном блоке есть repository.save()
).
Но когда я пытаюсь протестировать этот код с помощью @DataJpaTest
(база данных h2 в classpath для тестового окружения):
@DataJpaTest
class MyControllerTest {
@Autowired
private MyRepository repository;
@Test
void justForTest() {
var controller = new MyController(repository, new MyService());
assertThrows(RuntimeException.class, controller::justForTest);
var afterTestEntities = repository.findAll();
System.out.println(afterTestEntities.getFirst());
assertEquals(0, afterTestEntities.size());
}
}
Я получаю сбой теста, и мой отладочный вывод показывает мне мою сущность с состоянием неудачи MyEntity(id=1, status=FAIL)
. Но я предполагаю, что, как и в реальном запуске, у меня должно быть пустое хранилище из-за отката транзакции.
Таким образом, мой главный вопрос: как протестировать механизм @Transactional
в модульных тестах?
Это происходит потому, что тесты @DataJpaTest
по умолчанию являются транзакционными. Транзакция начинается перед каждым тестом и откатывается после его завершения. Очевидно, что вы не можете протестировать откат транзакции, находясь внутри транзакции. Вам нужно отключить распространение транзакций, используя аннотацию @Transactional
в вашем тесте.
@DataJpaTest
@Transactional(propagation = Propagation.NOT_SUPPORTED)
class MyNonTransactionalTests {
// ...
}
Для получения дополнительной информации смотрите Справочник по Spring Boot.
Также, если контроллер не является Spring Bean, @Transactional
не будет работать для него. Поэтому вам нужно будет добавить его в @Import
и внедрить в ваш тест с помощью @Autowired
.
@DataJpaTest
@Import({MyController.class})
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public class MyControllerTest {
@Autowired
private MyController myController;
// ...
}
Основная проблема заключается в том, что @DataJpaTest
не обрабатывает аннотацию @Transactional на вашем методе контроллера, потому что контроллер не управляется Spring в вашей тестовой настройке. Попробуйте добавить конфигурацию теста транзакций:
@Configuration
@EnableTransactionManagement
public class TransactionalTestConfig {
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
return new JpaTransactionManager(emf);
}
}
Затем в вашем классе теста:
@DataJpaTest
@Import({TransactionalTestConfig.class, MyService.class})
class MyControllerTest {
@Autowired
private MyRepository repository;
@Autowired
private PlatformTransactionManager transactionManager;
@Autowired
private MyService myService;
@Test
void testTransactionalRollback() {
// Создать шаблон транзакции
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
// Выполнить тест внутри транзакции
assertThrows(RuntimeException.class, () -> {
transactionTemplate.execute(status -> {
var controller = new MyController(repository, myService);
controller.justForTest();
return null;
});
});
// Проверить откат
var afterTestEntities = repository.findAll();
assertEquals(0, afterTestEntities.size());
}
}
в вашем свойстве:
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
logging.level.org.springframework.transaction=TRACE
.
Ответ или решение
Проблема с тестированием аннотации @Transactional в рамках @DataJpaTest
При тестировании методов, помеченных аннотацией @Transactional
, с использованием аннотации @DataJpaTest
может возникнуть закономерная проблема. Ваша цель — подтвердить механизм отката транзакции, который работает корректно в реальной среде, но не проявляет ожидаемого поведения в тестовом окружении. Давайте рассмотрим детали этой проблемы и возможные решения.
Проблема
Как вы указали, ваш контроллер содержит @Transactional
метод, который обрабатывает исключения и, в случае таких событий как RuntimeException
, является предопределенным для выполнения отката транзакции. В реальных условиях, при вызове этого метода через HTTP-запрос, поведение корректно, и все изменения в базе данных откатываются. Однако, тестирование этого поведения с @DataJpaTest
не дает ожидаемого результата — изменения в базе данных сохраняются.
Это происходит потому, что @DataJpaTest
ведет себя иначе, чем вы могли бы ожидать: он управляет транзакциями самостоятельно, используя механизм, который автоматически откатывает транзакции после выполнения каждого теста. Таким образом, вам не удается правильно протестировать логику отката, расположенную внутри @Transactional
метода вашего контроллера.
Решение
1. Деактивируйте аннотацию @Transactional в тестах
Первый шаг, который следует предпринять, — это провести тест вне управления транзакциями, заданного @DataJpaTest
. Вы можете сделать это, добавив аннотацию @Transactional(propagation = Propagation.NOT_SUPPORTED)
к вашему классу тестов:
@DataJpaTest
@Transactional(propagation = Propagation.NOT_SUPPORTED)
class MyControllerTest {
@Autowired
private MyRepository repository;
@Test
void justForTest() {
var controller = new MyController(repository, new MyService());
assertThrows(RuntimeException.class, controller::justForTest);
var afterTestEntities = repository.findAll();
assertEquals(0, afterTestEntities.size());
}
}
2. Импортируйте ваш контроллер
Поскольку контроллер не является бином Spring в контексте тестирования, необходимо проверить и убедиться в его импорте. Используйте аннотацию @Import
для этой цели:
@DataJpaTest
@Import(MyController.class)
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public class MyControllerTest {
@Autowired
private MyController myController;
// ...
}
3. Используйте конфигурацию теста с поддержкой транзакций
Если вы хотите иметь возможность тестировать механизм отката как таковой, вы можете создать специальную конфигурацию для тестирования:
@Configuration
@EnableTransactionManagement
public class TransactionalTestConfig {
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
return new JpaTransactionManager(emf);
}
}
Затем в вашем тесте подключите эту конфигурацию:
@DataJpaTest
@Import({TransactionalTestConfig.class, MyService.class})
class MyControllerTest {
@Autowired
private MyRepository repository;
@Autowired
private PlatformTransactionManager transactionManager;
@Test
void testTransactionalRollback() {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
assertThrows(RuntimeException.class, () -> {
transactionTemplate.execute(status -> {
myController.justForTest();
return null;
});
});
var afterTestEntities = repository.findAll();
assertEquals(0, afterTestEntities.size());
}
}
4. Свойства для отображения SQL
Для улучшения отладки в вашем файле настроек добавьте следующие параметры:
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
logging.level.org.springframework.transaction=TRACE
Эти параметры помогут вам видеть выполняемые запросы, что делает процесс тестирования более прозрачным.
Заключение
Тестирование методов с аннотацией @Transactional
в рамках @DataJpaTest
может оказаться непростой задачей, особенно если вы не учитываете особенности управления транзакциями, встроенные в Spring-окружение. Данное руководство предоставляет решения, позволяющие преодолеть эту сложность, и обеспечит надежное тестирование вашего бизнес-логики применительно к управлению транзакциями. Применив предложенные стратегии, вы сможете корректно отладить механизм отката и достичь высокой надежности ваших тестов.