본문으로 건너뛰기

스프링 테스트 격리

· 약 5분

테스트 격리

테스트의 순서에 따라 성공 실패 여부가 결정되는 비결정적인(non-determinism) 테스트가 되어서는 안되고, 테스트는 항상 순서에 상관없이 독립적으로 수행되도록 보장되어야 한다. 일반적으로 자원의 공유, 외부 API, 시간 등으로 비결정적인 테스트가 된다. 이를 해결하기 위해 테스트 대역을 사용하거나, 컨텍스트를 재실행하는 @DirtiesContext, 자원을 초기화하기 위해 테스트 이후에 테이블을 롤백 하는 @Transactional등 다양한 방법이 있다.
해당 글에서는 스프링에서 데이터베이스 자원의 공유를 방지하기 위해 테스트 격리를 수행하는 부분에 대해 설명한다.

Independent - FIRST

테스트끼리 서로 의존하면 안 된다.
서로 의존하게 된다면 하나의 테스트가 실패할 때, 또 다른 하나의 테스트가 실패할 수 있다.
다른 테스트에 의존하지 않고, 독립적으로 실행 가능한 테스트가 좋은 테스트다.

TestExecutionListener

스프링에서는 TextExecutionListner를 이용하여 각 테스트 실행 단계에서 이벤트를 수신할 수 있다.
이를 이용하면 JUnit의 @BeforeEach를 사용하는 것과 유사하게, 테스트의 생명주기 이전 또는 이후에 필요한 작업을 실행시킬 수 있다.

public interface TestExecutionListener {
default void beforeTestClass(TestContext testContext) throws Exception {}
default void prepareTestInstance(TestContext testContext) throws Exception {}
default void beforeTestMethod(TestContext testContext) throws Exception {}
default void beforeTestExecution(TestContext testContext) throws Exception {}
default void afterTestExecution(TestContext testContext) throws Exception {}
default void afterTestMethod(TestContext testContext) throws Exception {}
default void afterTestClass(TestContext testContext) throws Exception {}
}

AbstractTestExecutionListener 상속하여 구현

AbstractTestExecutionListener를 상속받아 테스트 격리 환경을 만들어주는 클래스로, 인터페이스인 TextExecutionListner와 달리 Ordered가 구현되어 있어 해당 클래스를 상속받아 구현한 클래스는 프레임워크가 제공하는 리스너 다음에 실행시키도록 해준다.
다음과 같이 데이터베이스에서 각각의 테이블에 해당하는 Truncate 쿼리를 만들어서 조회하고, Test 메서드가 끝날때 마다 해당 쿼리를 실행하여 테이블을 초기화시키도록 설정한다.


public class DatabaseCleaner extends AbstractTestExecutionListener {

private static final String TRUNCATE_TABLE_QUERY = """
SELECT Concat('TRUNCATE TABLE ', TABLE_NAME, ';')
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = 'PUBLIC'
""";

@Override
public void afterTestMethod(TestContext testContext) {
JdbcTemplate jdbcTemplate = getJdbcTemplate(testContext);
List<String> truncateTableQueries = getTruncateTableQueries(jdbcTemplate);
truncateTables(jdbcTemplate, truncateTableQueries);
}

private JdbcTemplate getJdbcTemplate(TestContext testContext) {
return testContext.getApplicationContext().getBean(JdbcTemplate.class);
}

private List<String> getTruncateTableQueries(JdbcTemplate jdbcTemplate) {
return jdbcTemplate.queryForList(TRUNCATE_TABLE_QUERY, String.class);
}

private void truncateTables(JdbcTemplate jdbcTemplate, List<String> truncateTableQueries) {
jdbcTemplate.execute("SET REFERENTIAL_INTEGRITY FALSE");
truncateTableQueries.forEach(jdbcTemplate::execute);
jdbcTemplate.execute("SET REFERENTIAL_INTEGRITY TRUE");
}
}

Listener 등록

@TestExecutionListeners를 이용하여 사용자 정의 리스너를 등록할 수 있다.
mergeMode의 기본값은 REPLACE_DEFAULTS로 리스너가 이미 존재하는 경우 등록된 리스너로 변경된다.
MERGE_WITH_DEFAULTS로 설정한다면 Ordered 기준으로 순서가 결정된다.
이후 격리가 필요한 테스트들은 다음의 추상 클래스를 상속하여 사용하면 된다.


@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestExecutionListeners(
value = DatabaseCleaner.class,
mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
)
public abstract class AcceptanceTest {

@LocalServerPort
private int port;

@BeforeEach
public void setUp() {
RestAssured.port = port;
}
}

참고 자료

The Spring TestExecutionListener, Baeldung
인수테스트에서 테스트 격리하기, 테코블
Eradicating Non-Determinism in Tests, martin fowler
@SpringBootTest의 테스트 격리시키기, MangKyu