서론
통합테스트나 인수테스트를 위해 @SpringBootTest를 이용하는 테스트 환경에서 테스트들을 격리하는 방법들 중 주로 널리 알려진 것들에 대해 정리하고 TestExecutionListener 사용하여 격리했던 방법을 공유해보고자 한다.
테스트 격리란
테스트 격리란 테스트 순서에 상관없이 독립적으로 실행되며, 결정적으로 동작되는 것을 의미한다.
비결정적인 테스트는 쉽게 말해서 테스트 실행 시 같은 입력값에 대해 항상 같은 출력하지 않는 테스트를 의미한다.
따라서 "결정적으로 동작"의 의미란 멱등성과 비슷하다. 같은 입력 값이면 항상 같은 결과를 반환하는 것을 의미한다.
@SpringBootTest 테스트 격리 방법
@Transactional
첫 번째로 트랜잭션 어노테이션이다. 많이 알려진 내용으로 해당 어노테이션을 테스트 코드에 사용하면 테스트가 종료되고 자동으로 롤백하는 기능을 이용하는 것이다.
test transactions will be automatically rolled back after completion of the test.
하지만 해당 방법은 API 접점에서 검증하는 E2E 테스트를 인수테스트의 의도로 인수테스트 환경을 구축했다면 @Transactional 어노테이션으로는 테스트 격리를 하기 힘들 수도 있다.
인수테스트는 보통 블랙박스 성격의 테스트이므로 WebTestClient 혹은 RestAssured을 이용하는 경우가 많은데 그러려면 @SpringBootTest 어노테이션을 RANDOM_PORT나 DEFINED_PORT를 이용하여 실제 웹 한경을 구성하기 때문이다
따라서 포트 번호를 따로 주어 WebEnvironment를 구성하는 경우 Http client와 테스트 서버는 별도의 스레드에서 수행되기 때문에 자동 롤백이 이루어지지 않는다.
@DirtiesContext
효과적인 테스트 수행을 위해 스프링에서는 context caching 기능을 지원한다. 해당 어노테이션은 테스트와 관련된 ApplicationContext가 Dirty하기 때문에 Context Cache에서 종료되고 삭제되어야 함을 나타내는 Test Annotation이다.
@DirtiesContext 어노테이션을 통해 테스트를 수행하기 전, 수행한 이후, 그리고 테스트의 각 테스트 케이스마다 수행하기 전, 수행한 이후에 context를 다시 생성하도록 지시하여 테스트를 격리할 수 있다.
하지만 매 테스트마다 Application Context를 매번 생성해야 하기 때문에 테스트 속도가 현저히 느려진다.
테스트 코드상으로 매번 삭제
테스트에 필요한 데이터를 JUnit 생명주기인 @BeforeEach, @AfterEach를 활용하여 테스트가 시작되기 전이나 후에 데이터들을 삭제하여 테스트 격리를 하는 방식이다. application context를 매번 띄우는 것보다 낮은 비용이므로 속도 측면에서 효율적이라고 볼 수 있다.
하지만 이 방식의 단점은 생성해야 할 데이터가 많거나, 연관관계를 모를 경우 제대로 데이터를 삭제하지 못하는 문제가 발생할 수 있다.
또한, 테스트 클래스도 길어지기에 가독성도 안 좋을 수 있다.
TRUNCATE를 통한 테이블 초기화
TRUNCATE (DDL)는 DELETE (DML)와 다르게 행마다 락을 걸지 않고, 트랜잭션 로그 공간을 적게 사용하므로 초기화하는 속도가 더 빠르다.
테스트 격리를 위해 TRUNCATE 하는 방법은 보통 크게 2가지이다.
- @Sql 사용
- 코드에서 truncate 쿼리 사용
@Sql
@Sql은 Spring Boot에서 제공하는 애노테이션이며, 클래스 테스트가 실행되기 전 @Sql이 가리키는 경로에 있는 SQL문이 먼저 실행되게 된다. 이 SQL 파일 안에 TRUNCATE관련 내용을 넣어두는 방식을 통해 DB 테스트 격리를 할 수 있다.
하지만 테이블이 추가/변경/삭제될 때마다 수정이 필요하므로 관리가 필요하다.
코드에서 테이블 TRUNCATE 쿼리 사용
JPA 사용 시 EntityManager를 이용하여 혹은 jpa를 사용하지 않는 경우 DataSource를 이용하여 테이블 이름을 조회하여 각 테이블들을 truncate 시켜주는 쿼리를 수행하는 것이다. 해당 방법을 사용하면 테이블 상태에 의존하지 않는 초기화 환경 구축 가능하다.
해당 방법을 사용해서 테스트 격리를 적용해 보자.
코드에서 테이블 TRUNCATE 쿼리 사용 - TestExecutionListener 이용하기
TestExecutionListener
TestExecutionListener는 스프링에서 제공하는 테스트 실행 주기에서 콜백 메서드를 통해 사용자가 추가 작업을 수행하도록 지원하는 인터페이스이다.
사실 위에서 살펴본 transactional test의 기본 롤백, @DirtiesContext, @Sql의 기능들 모두 해당 TestExecutionListener 인터페이스를 상속받은 기본 리스너를 통해 스프링에서 제공하고 있는 것이다.
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 afterTestMethod(TestContext testContext) throws Exception {};
default void afterTestClass(TestContext testContext) throws Exception {};
}
- beforeTestClass()
- junit의 @BeforeAll와 같다.
- 모든 테스트를 실행하기 전 단 한 번만 실행/호출
- prepareTestInstance()
- junit에서 X
- 테스트 인스턴스가 준비되었을 때 호출
- beforeTestMethod()
- junit의 @BeforeEach
- 각 테스트 메서드 실행 전에 실행/호출
- afterTestExecution()
- junit에서 X
- 각 테스트 메서드 실행 직후 호출
- afterTestMethod()
- junit에서 @AfterEach
- 각 테스트 메서드 실행 후에 실행/호출
- afterTestClass()
- junit의 @AfterAll
- 모든 테스트를 실행한 후 단 한 번만 실행/호출
Custom TestExecutionListener를 구현
sql 파일이 아닌 코드에서 테이블 TRUNCATE를 위해 AbstractTestExecutionListener 이용해 보자.
해당 커스텀 리스너를 등록하기 위해서는 @TestExecutionListeners 어노테이션을 통해 등록하면 되는데 기본 제공되는 테스트 리스너들이 필요하다면 MERGE_WITH_DEFAULTS 모드를 사용하도록 하자.
그리고 주의할 점은 TestExecutionListeners 들은 AnnotationAwareOrderComparator를 통해 정렬되어 순서대로 핸들링되기 때문에 순서에 주의하자.
따라서 AcceptanceTestExecutionListener 클래스에 마지막 순서로 동작하기 위해 Ordered 인터페이스를 구현하도록 추가하고 어노테이션을 붙여 테스트 격리를 진행하였다.
사용 및 확인
이렇게 테스트 메서드 종료시마다 truncate 쿼리가 나가는 것을 확인할 수 있다. 물론 해당 방법이 가장 좋은 방법인 것은 아닐 것이다.
따라서 각각 자신의 상황에서 추구하는 테스트 격리의 정의와 격리를 위한 장치가 필요한지 살펴보며 적용하는 것이 가장 좋은 방법일 것이라고 생각한다.
참고
- https://velog.io/@appti/TestExecutionListener을-활용한-테스트-격리
- https://www.baeldung.com/spring-testexecutionlistener
- https://tecoble.techcourse.co.kr/post/2020-09-15-test-isolation/
- https://mangkyu.tistory.com/264
- https://github.com/binghe819/TIL/blob/master/Spring/Test/DB 테스트 격리/DB 테스트 격리.md#2-3-dirtiescontext-사용
- https://edu.nextstep.camp/
'spring' 카테고리의 다른 글
[spring data mongo] Query를 Type-safe 하게 작성하기 (작성 중) (0) | 2024.11.19 |
---|---|
WebClient의 DataBufferLimitException 해결방법 (0) | 2024.11.15 |
Spring Async (0) | 2024.08.02 |
Spring Webclient (0) | 2024.08.02 |
Spring AOP (0) | 2024.08.02 |