이 글은 다음 자료를 참고하여 작성하였습니다.

테스트 격리

테스트 코드를 안정적으로 유지하기 위해선 테스트가 실행되는 환경을 제어할 수 있어야합니다. 예를 들어 하나의 테스트가 데이터베이스의 데이터를 수정하고 그대로 둔다면 다른 테스트가 영향을 받아 실패할 수 있습니다. 즉, 테스트 환경을 제어하지 않는다면 테스트 사이에 의존 관계가 생길 수 있고 실행 순서에 따라 성공 여부가 달라질 수 있습니다.

이때, Martin Fowler는 테스트 환경을 격리하기 위한 두 가지 방법을 제안합니다.

  • 테스트를 시작할 때마다 초기화
  • 테스트가 끝날 때 원래대로 복구

전자의 경우 초기 상태를 제대로 구축하지 않아 테스트가 실패하는 경우 쉽게 확인할 수 있다는 장점이 있으나 매번 데이터베이스를 초기화하는 것에서 많은 시간이 소요될 수 있습니다.

Spring에서 테스트 격리

Untitled.png

Spring Test에서는 두 가지 방법 중 후자를 선택하고 있으며 이는 @Transactional에 의해 작동합니다. 그렇다면 이러한 테스트 격리를 어디에서 처리하고 있을까요?

Spring TestContext framework

그전에 Spring의 테스트 환경에 대해 알아보겠습니다. Spring에서는 TestContext framework을 이용하여 사용 중인 테스트 프레임워크와 무관하게 annotation 기반 테스트 환경을 구축할 수 있습니다. 프레임워크는 다음과 같은 주요 인터페이스들로 구성됩니다.

  • TestContextManager
  • TestContext
  • TestExecutionListener
  • SmartContextLoader

TestContextManager는 각각의 테스트 클래스마다 생성되며 해당 테스트 인스턴스의 컨텍스트를 보유하는 TestContext를 관리합니다. 또, 테스트가 진행됨에 따라 TestContext의 상태를 업데이트하고, 테스트 생명 주기에 따라 TestExecutionListener의 작업을 실행합니다.

SmartContextLoarder는 주어진 테스트 클래스에 대한 ApplicationContext 로드를 담당합니다.

Spring 테스트 실행 이벤트

Untitled.png

TestContextManager에서 처리하는 이벤트는 위와 같습니다. 각각의 이벤트마다 TestExecutionListener의 메서드들을 실행합니다.

@Transactional로 테스트 격리하기

TestContext framework에서 트랜잭션은 TransactionalTestExecutionListener에 의해 관리됩니다. 테스트 메서드에 @Transactional을 달면 테스트가 트랜잭션 내에서 실행되며 기본적으로 테스트가 완료된 후 자동으로 롤백됩니다.

트랜잭션은 @BeforeAll, @BeforeEach와 같은 테스트 생명 주기 메서드에는 지원되지 않습니다. 또한, 트랜잭션 전파 속성이 NOT_SUPPORTED 혹은 NEVER로 설정된 경우 트랜잭션 내에서 실행되지 않습니다.

지금부터 Spring의 테스트 격리 과정을 코드 레벨에서 알아보도록 하겠습니다.

Event: before test setup

before test setup 이벤트는 테스트 메서드가 실행 될 때를 말합니다. 이때 TestContextManager는 TestExecutionListener들의 beforeTestMethod()를 실행합니다.

Untitled.png

for문을 통해 TransactionTestExecutionListenerbeforeTestMethod()를 실행하며 해당 메서드에서 주목해야할 점은 다음과 같습니다.

Untitled.png

특정 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()를 실행합니다.

Untitled.png

for문을 통해 TransactionTestExecutionListenerafterTestMethod()를 실행하며 해당 메서드에서 주목해야할 점은 다음과 같습니다.

Untitled.png

TransactionContext를 이용하여 트랜잭션을 종료합니다.

Untitled.png

이때 TransactionContext의 flaggedForRollback 상태에 따라 트랜잭션을 종료할 때 _commit_할 것인지 _rollback_할 것인지 결정합니다.

flaggedForRollback는 테스트 메서드를 실행할 때 Rollback 정보를 저장한 필드이며 값이 ture일 경우 _rollback_을, false일 경우 _commit_을 하며 트랜잭션을 종료합니다.

롤백하기 싫어잉

이렇게 Spring에서 테스트 격리를 위해 @Transactional을 사용하고 테스트가 끝났을 때 롤백하는 과정에 대해 알아보았습니다. 만약 모종의 이유로 인하여 롤백을 하지 않으려면 다음과 같은 annotation을 테스트 메서드 위에 작성하면 됩니다.

  • @Rollback(value = false)
  • @Commit

이때, @Commit을 사용하는 경우 @Rollback과 함께 사용해선 안됩니다. 이는 Spring에서 지원하지 않으며 예측할 수 없는 테스트 결과가 발생할 수 있습니다.

Untitled.png

한편 @Commit의 모습은 위와 같으며 @Rollback(value = false)의 가독성을 개선한 것으로 실제 롤백 로직은 위에서 언급한 @Rollback을 기반으로 작동합니다.

카테고리:

업데이트:

댓글남기기