테스트에서 @Sql 로 테스트 데이터가 들어가지 않았던 이유 (with. custom TestExecutionListener)

cooper·2023년 12월 30일
0
post-thumbnail

문제 상황

친구와 토이 프로젝트를 하며 테스트를 위한 데이터를 추가하고자 @Sql annotation 을 적용하고자 했다. 하지만 테스트 데이터가 들어가지 않는 이유를 찾기 위한 삽질 로그를 기록하고자 한다.

Background

1. SpringBootTest.WebEnvironment.RANDOM_PORT 사용

실제 개발 환경과 유사한 환경을 위해 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 : 실제 웹 환경을 구성 + 지정한 포트에서 동작


2. custom TestExecutionListener 사용

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 순서 문제였다. TestExecutionListener 는 개발자가 명시한 순서에 따라 실행된다. custom TestExecutionListener 작성시 별도로 순서(Ordered) 를 설정이 없으면 우선 순위가 가장 낮게 설정되어 가장 늦게 수행한다. 반면, @Sql 을 동작시키는 리스너인 SqlScriptsTestExecutionListener 를 확인해보면 순서(Ordered)가 선언되어 있다. 즉, @Sql 실행하고 TRUNCATE 로직이 실행되어 데이터 추가나서 다시 초기화되는 문제였다.(@Sql 실행 → TRUNCATE 실행 🫠)

해결 : custom TestExecutionListener 순서 입력

TRUNCATE 로직 이후에 @Sql 실행되기 위해 Order 값을 SqlScriptsTestExecutionListener 의 값보다 낮게 설정했다. ( TRUNCATE 실행@Sql 실행)

public class DataCleanupTestExecutionListener extends AbstractTestExecutionListener {

	@Override
	public int getOrder() {
		return 4500;
	}
    
}

정상적으로 테스트 데이터가 등록되었다. 👍

오늘의 교훈

custom TestExecutionListener 만들 때는 기존의 TestExecutionListener 와의 순서를 비교해서 값을 추가하자.

기타 지식(properties)

  1. sql.init.mode : 스크립트 동작 설정
    • ALWAYS: 모든 데이터베이스에 sql 스크립트를 동작시킨다.
  2. spring.jpa.defer-datasource-initialization: true
    • 2.5이상의 버전부터 data.sql 스크립트는 Hibernate가 초기화되기 전에 실행
    • hibernate ddl-auto property 가 동작하고 data.sql 이 동작하도록 하는 옵션

Reference

profile
막연함을 명료함으로 만드는 공간 😃

0개의 댓글