Spring Test에서 테스트 환경을 격리하는 방법
이 글은 다음 자료를 참고하여 작성하였습니다.
- Martin Fowler - Eradicating Non-Determinism in Tests
- JUnit 5 User Guide
- Spring TestContext Framework documentation
- Spring Boot 3 code
테스트 격리
테스트 코드를 안정적으로 유지하기 위해선 테스트가 실행되는 환경을 제어할 수 있어야합니다. 예를 들어 하나의 테스트가 데이터베이스의 데이터를 수정하고 그대로 둔다면 다른 테스트가 영향을 받아 실패할 수 있습니다. 즉, 테스트 환경을 제어하지 않는다면 테스트 사이에 의존 관계가 생길 수 있고 실행 순서에 따라 성공 여부가 달라질 수 있습니다.
이때, Martin Fowler는 테스트 환경을 격리하기 위한 두 가지 방법을 제안합니다.
- 테스트를 시작할 때마다 초기화
- 테스트가 끝날 때 원래대로 복구
전자의 경우 초기 상태를 제대로 구축하지 않아 테스트가 실패하는 경우 쉽게 확인할 수 있다는 장점이 있으나 매번 데이터베이스를 초기화하는 것에서 많은 시간이 소요될 수 있습니다.
Spring에서 테스트 격리
Spring Test에서는 두 가지 방법 중 후자를 선택하고 있으며 이는 @Transactional
에 의해 작동합니다. 그렇다면 이러한 테스트 격리를 어디에서 처리하고 있을까요?
Spring TestContext framework
그전에 Spring의 테스트 환경에 대해 알아보겠습니다. Spring에서는 TestContext framework을 이용하여 사용 중인 테스트 프레임워크와 무관하게 annotation 기반 테스트 환경을 구축할 수 있습니다. 프레임워크는 다음과 같은 주요 인터페이스들로 구성됩니다.
- TestContextManager
- TestContext
- TestExecutionListener
- SmartContextLoader
TestContextManager
는 각각의 테스트 클래스마다 생성되며 해당 테스트 인스턴스의 컨텍스트를 보유하는 TestContext를 관리합니다. 또, 테스트가 진행됨에 따라 TestContext의 상태를 업데이트하고, 테스트 생명 주기에 따라 TestExecutionListener의 작업을 실행합니다.
SmartContextLoarder
는 주어진 테스트 클래스에 대한 ApplicationContext 로드를 담당합니다.
Spring 테스트 실행 이벤트
TestContextManager
에서 처리하는 이벤트는 위와 같습니다. 각각의 이벤트마다 TestExecutionListener
의 메서드들을 실행합니다.
@Transactional로 테스트 격리하기
TestContext framework에서 트랜잭션은 TransactionalTestExecutionListener
에 의해 관리됩니다. 테스트 메서드에 @Transactional을 달면 테스트가 트랜잭션 내에서 실행되며 기본적으로 테스트가 완료된 후 자동으로 롤백됩니다.
트랜잭션은 @BeforeAll, @BeforeEach와 같은 테스트 생명 주기 메서드에는 지원되지 않습니다. 또한, 트랜잭션 전파 속성이 NOT_SUPPORTED 혹은 NEVER로 설정된 경우 트랜잭션 내에서 실행되지 않습니다.
지금부터 Spring의 테스트 격리 과정을 코드 레벨에서 알아보도록 하겠습니다.
Event: before test setup
before test setup 이벤트는 테스트 메서드가 실행 될 때를 말합니다. 이때 TestContextManager
는 TestExecutionListener들의 beforeTestMethod()
를 실행합니다.
for문을 통해 TransactionTestExecutionListener
의 beforeTestMethod()
를 실행하며 해당 메서드에서 주목해야할 점은 다음과 같습니다.
특정 TestContext에 대한 트랜잭션 컨텍스트를 관리하는 TransactionContext를 생성할 때 isRollback()
을 호출하여 테스트가 끝날 때 롤백할 것인지에 대한 정보를 TransactionContext에 저장합니다.
롤백 여부는 테스트 메서드의 @Rollback의 value에서 추출합니다. @Rollback의 default value는 true이며, @Rollback을 명시적으로 작성하지 않은 경우엔 true를 반환합니다.
이후 TransactionContext가 생성되면 테스트 메서드를 감싸는 트랜잭션을 시작합니다.
Event: after test tear down
after test tear down 이벤트는 테스트 메서드가 종료될 때를 말합니다. 이때 TestContextManager
는 TestExecutionListener들의 afterTestMethod()
를 실행합니다.
for문을 통해 TransactionTestExecutionListener
의 afterTestMethod()
를 실행하며 해당 메서드에서 주목해야할 점은 다음과 같습니다.
TransactionContext를 이용하여 트랜잭션을 종료합니다.
이때 TransactionContext의 flaggedForRollback
상태에 따라 트랜잭션을 종료할 때 _commit_할 것인지 _rollback_할 것인지 결정합니다.
flaggedForRollback
는 테스트 메서드를 실행할 때 Rollback 정보를 저장한 필드이며 값이 ture일 경우 _rollback_을, false일 경우 _commit_을 하며 트랜잭션을 종료합니다.
롤백하기 싫어잉
이렇게 Spring에서 테스트 격리를 위해 @Transactional을 사용하고 테스트가 끝났을 때 롤백하는 과정에 대해 알아보았습니다. 만약 모종의 이유로 인하여 롤백을 하지 않으려면 다음과 같은 annotation을 테스트 메서드 위에 작성하면 됩니다.
- @Rollback(value = false)
- @Commit
이때, @Commit을 사용하는 경우 @Rollback과 함께 사용해선 안됩니다. 이는 Spring에서 지원하지 않으며 예측할 수 없는 테스트 결과가 발생할 수 있습니다.
한편 @Commit의 모습은 위와 같으며 @Rollback(value = false)의 가독성을 개선한 것으로 실제 롤백 로직은 위에서 언급한 @Rollback을 기반으로 작동합니다.
댓글남기기