친구와 토이 프로젝트를 하며 테스트를 위한 데이터를 추가하고자 @Sql annotation 을 적용하고자 했다. 하지만 테스트 데이터가 들어가지 않는 이유를 찾기 위한 삽질 로그를 기록하고자 한다.
실제 개발 환경과 유사한 환경을 위해 RANDOM_PORT
를 설정했다. 해당 옵션을 설정하면 클라이언트와 서버가 분리되어 멀티 스레드 환경이 된다. 그러므로 Thread Local 기반으로 동작하는 @Transaction
을 사용하여 롤백 처리를 할 수 없다.
롤백을 해야 하는 이유는 테스트 격리(Isolated Test) 을 위함이다. 테스트 격리는 공유 자원을 사용하는 여러 테스트끼리 격리하여 서로 영향을 끼치지 않는 방법을 말하여 대표적인 예시가 DB 데이터 이다. DB 데이터 격리를 위해 롤백을 하며, 스프링에서는 일반적으로 동일 스레드에서 동작하여 @Transactional
을 사용하여 롤백한다. 하지만 앞서 이야기했 듯, 이 방법으로는 롤백을 할 수 없어 RDB 에서 제공하는 TRUNCATE
명령어를 활용하고자 했다.
webEnvironment option in @SpringBootTest
MOCK
: Mocking된 웹 환경을 제공(MockMvc를 사용한 테스트 가능 = 가짜 웹 환경에서 테스트)
RANDOM_PORT
: 실제 웹 환경을 구성 (실제 웹 환경(with tomcat) 에서 테스트)
DEFINED_PORT
: 실제 웹 환경을 구성 + 지정한 포트에서 동작
sql 파일을 활용하는 방법도 있지만 스키마 변경(e.g. 추가, 삭제) 때마다 코드를 수정해야 하는 번거로움이 있기 때문에 INFORMATION_SCHEMA.TABLES
(mysql 8.0 기준) 에 존재하는 스키마 정보를 가져와 데이터를 초기화하는 AcceptanceTestExecutionListener 를 작성했다.
Spring testContext 는 테스트를 위한 확장, 편의 기능을 TestExecutionListener
를 제공한다. 즉, 편리한 테스트를 위한 편의 기능을 TestExecutionListener 를 통해 제공한다. 대표적인 예시로 MockitoTestExecutionListener
이 존재한다. @MocbBean 어노테이션을 선언하기만 하면 테스트 이전에 해당 리스너에서 모킹(mocking) 하고 테스트가 끝난 시점에서는 모킹을 제거한다.
public class AcceptanceTestExecutionListener extends AbstractTestExecutionListener {
@Override
public void beforeTestMethod(final TestContext testContext) {
final JdbcTemplate jdbcTemplate = getJdbcTemplate(testContext);
final List<String> truncateQueries = getTruncateQueries(jdbcTemplate);
truncateTables(jdbcTemplate, truncateQueries);
}
private List<String> getTruncateQueries(final JdbcTemplate jdbcTemplate) {
try (Connection connection = jdbcTemplate.getDataSource().getConnection()) {
String schema = connection.getCatalog();
return jdbcTemplate.queryForList(
"SELECT concat('TRUNCATE TABLE ', TABLE_NAME) "
+ "FROM INFORMATION_SCHEMA.TABLES "
+ "WHERE TABLE_SCHEMA = '" + schema + "'", String.class);
} catch (Exception exception) {
throw new RuntimeException();
}
}
private JdbcTemplate getJdbcTemplate(final TestContext testContext) {
return testContext.getApplicationContext().getBean(JdbcTemplate.class);
}
private void truncateTables(final JdbcTemplate jdbcTemplate, final List<String> truncateQueries) {
execute(jdbcTemplate, "SET FOREIGN_KEY_CHECKS = FALSE");
truncateQueries.forEach(v -> execute(jdbcTemplate, v));
execute(jdbcTemplate, "SET FOREIGN_KEY_CHECKS = TRUE");
}
private void execute(final JdbcTemplate jdbcTemplate, final String query) {
jdbcTemplate.execute(query);
}
}
테스트를 위해 테스트 데이터를 @Sql 어노테이션을 통해 주입하고자 했지만 데이터가 추가되지 않았다.
@DisplayName("인증 테스트")
@Sql("classpath:member.sql")
public class AuthenticationTest {
...
}
원인은 TestExecutionListener 순서 문제였다. TestExecutionListener 는 개발자가 명시한 순서에 따라 실행된다. custom TestExecutionListener 작성시 별도로 순서(Ordered) 를 설정이 없으면 우선 순위가 가장 낮게 설정되어 가장 늦게 수행한다. 반면, @Sql 을 동작시키는 리스너인 SqlScriptsTestExecutionListener 를 확인해보면 순서(Ordered)가 선언되어 있다. 즉, @Sql 실행하고 TRUNCATE 로직이 실행되어 데이터 추가나서 다시 초기화되는 문제였다.(@Sql 실행 → TRUNCATE 실행
🫠)
TRUNCATE 로직 이후에 @Sql 실행되기 위해 Order 값을 SqlScriptsTestExecutionListener 의 값보다 낮게 설정했다. ( TRUNCATE 실행
→ @Sql 실행
)
public class DataCleanupTestExecutionListener extends AbstractTestExecutionListener {
@Override
public int getOrder() {
return 4500;
}
}
정상적으로 테스트 데이터가 등록되었다. 👍
custom TestExecutionListener 만들 때는 기존의 TestExecutionListener 와의 순서를 비교해서 값을 추가하자.