테스트 코드 작성시 @Transactional 사용하지 않기
테스트 코드에서 @Transactional은 데이터베이스도 롤백이 되기 때문에 @BeforeEach에서 일일이
repository를 deleteAll 해주지 않고 외래키 문제로 고민할 필요도 없기 때문에 편리합니다.
그래서 자주 사용을 했었는데 한가지 문제점이 있습니다.
바로 원래 코드에서 @Transactional이 없더라도 테스트 코드에서 @Transactional을 사용한다면
하나의 트랜잭션으로 묶여 원래 코드와 다른 결과를 나타낼 수 있다는 점입니다.
이 문제점은 예전 글에서 작성한 적이 있습니다.
JPA 변경감지 적용이 안될 때
JPA 변경 감지 적용이 안된다? 프로젝트에서 리뷰 수정 기능을 구현하고 있는데 JPA에서는 변경 감지(더티 체킹)를 작동한다는 사실을 알고 있었다. 영속성이 유지되는 엔티티라면 update 쿼리를 따
320hwany.tistory.com
'위 테스트가 실패를 했다. 이유는 테스트 코드에도 @Transactional을 적용해줘야 하기 때문이다.
@Transaction을 적용해주니 테스트가 통과하였다. 하지만 이때 Service의 @Transaction을 지워도
테스트에 @Transaction이 있어서 테스트가 통과하였다. 애플리케이션의 작동 여부를 체크하기 위한
테스트 코드인데 뭔가 찝찝한 느낌이다... 혹시 다른 방법이 있으려나... 찾으면 다시 글을 작성하기로..'
위 글에서 이러한 문장을 작성한 적이 있는데 더 좋은 방법을 찾아 이번 글에서 정리해보려고 합니다.
먼저 참고한 자료입니다
[Spring] @SpringBootTest의 테스트 격리시키기(TestExecutionListener), @Transactional로 롤백되지 않는 이유
이번에 넥스트스텝 ATDD 강의를 듣게 되었습니다. 과제 중에 @SpringBootTest를 사용하는 테스트들을 격리시키는 부분이 있었는데, 제가 사용했던 방법을 공유하도록 하겠습니다. 1. SpringBootTest가 @Tran
mangkyu.tistory.com
위 글에서 말하는 것은 '모든 테스트를 서로 독립적으로 실행하고 격리시키기 위해 테이블 명세만
남기고 모든 데이터를 제거하는 TRUNCATE 명령어를 모든 테이블에 수행하자' 라는 것 입니다.
위 글에서 정말 정말 자세히 설명이 되어있기 때문에 저는 간단히 사용법만 정리하겠습니다.
AcceptanceTestExecutionListener
public class AcceptanceTestExecutionListener extends AbstractTestExecutionListener {
@Override
public void afterTestMethod(final TestContext testContext) {
final JdbcTemplate jdbcTemplate = getJdbcTemplate(testContext);
final List<String> truncateQueries = getTruncateQueries(jdbcTemplate);
truncateTables(jdbcTemplate, truncateQueries);
}
private JdbcTemplate getJdbcTemplate(final TestContext testContext) {
return testContext.getApplicationContext().getBean(JdbcTemplate.class);
}
private List<String> getTruncateQueries(final JdbcTemplate jdbcTemplate) {
return jdbcTemplate.queryForList("SELECT Concat('TRUNCATE TABLE ', TABLE_NAME,
';') " + "AS q FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA
= 'PUBLIC'", String.class);
}
private void truncateTables(final JdbcTemplate jdbcTemplate,
final List<String> truncateQueries) {
execute(jdbcTemplate, "SET REFERENTIAL_INTEGRITY FALSE");
truncateQueries.forEach(v -> execute(jdbcTemplate, v));
execute(jdbcTemplate, "SET REFERENTIAL_INTEGRITY TRUE");
}
private void execute(final JdbcTemplate jdbcTemplate, final String query) {
jdbcTemplate.execute(query);
}
}
H2 인메모리 DB를 사용할 때 위와 같이 쿼리문을 작성한 메소드를 만듭니다.
AcceptanceTest
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Retention(RetentionPolicy.RUNTIME)
@TestExecutionListeners(value = {AcceptanceTestExecutionListener.class,},
mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
public @interface AcceptanceTest {
}
여러 메타 에노테이션을 섞은 합성 에노테이션을 만듭니다.
@AcceptanceTest
public class ServiceTest {
...
}
테스트를 작성할 때 AcceptanceTest 에노테이션을 사용하면 됩니다.
이렇게하면 @Transactional을 사용할 때의 문제점도 해결하고 @BeforeEach를 사용하여
직접 deleteAll 할 때 외래키문제로 오류가 발생하는 문제도 해결하여 독립적인 테스트를
작성할 수 있습니다!!
추가
테스트 코드에서 @Trasactional 사용에 대한 논쟁에 대해 정리하고 제가 사용하는 방법을 정리해보았습니다.
테스트 코드에서 @Transactional 사용에 대한 끊임 없는 논쟁
각종 블로그 글, 유튜브, 페이스북, 오픈 카톡방, 인프런 글 등을 보다 보면 테스트 코드 작성시 @Transactional의 사용 여부는 끊임없이 논쟁되는 주제 같습니다. 여러 글들을 많이 보았는데 향로님
320hwany.tistory.com