테스트 격리 (Test Isolation)
✔️ 테스트 격리 (Test Isolation)
각 테스트가 서로 독립적으로 실행되도록 보장하는 것을 말합니다. 즉, 어떤 테스트가 실행되더라도 다른 테스트의 결과나 상태에 영향을 주거나 받지 않아야 한다는 의미입니다.
테스트 격리가 중요한 이유는 격리가 제대로 이루어지지 않으면 비결정적 테스트
가 발생할 수 있기 때문입니다. 비결정적(Non-deterministic) 테스트
는 같은 테스트를 여러 번 실행했을 때 항상 같은 결과를 내지 않는 테스트를 말합니다. 예를 들어, 테스트가 데이터베이스와 같은 공유 자원에 의존할 경우 실행 순서나 다른 테스트의 실행 여부에 따라 성공 또는 실패 결과가 달라질 수 있습니다. 비결정적 테스트
는 실패했을 때 실제 코드의 문제인지, 비결정적 요인 때문인지 판단하기 어려워집니다. 따라서 테스트가 항상 동일한 조건에서 예측 가능한 결과를 낼 수 있도록 격리하는 것이 중요합니다.
✔️ Spring에서 같은 데이터베이스를 사용하는 테스트는 어떻게 격리할 수 있을까?
@DirtiesContext
어노테이션 사용
Spring의 테스트 컨텍스트는 애플리케이션 컨텍스트를 캐싱해서 각각의 테스트에서 재사용합니다. @DirtiesContext
어노테이션을 사용하면 테스트마다 새로운 애플리케이션 컨텍스트를 로드하여 완전한 격리를 보장할 수 있습니다. 하지만 애플리케이션 컨텍스트를 매번 새로 로드하는 것은 비용이 크고 시간이 오래 걸리는 작업이기 때문에 성능이 저하된다는 단점이 있습니다.
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
@SpringBootTest
class MyIntegrationTest {
// ...
}
@Sql
어노테이션 사용
@Sql
어노테이션을 사용하면 테스트 실행 전 또는 후에 특정 SQL
스크립트를 실행할 수 있습니다. 이때 TRUNCATE
DDL을 사용하여 테이블 자체를 비움으로써 테스트 간 독립된 테이블을 사용할 수 있습니다. 하지만 테이블이 추가될 때마다 SQL
스크립트를 수정해야 하기 때문에 유지보수 비용이 발생한다는 단점이 있습니다.
@Sql("/truncate.sql")
@SpringBootTest
class MyIntegrationTest {
// ...
}
@Transactional
어노테이션 사용
@Transactional
어노테이션을 사용하면 테스트가 실행된 후 트랜잭션을 롤백하여 데이터베이스 상태를 원래대로 유지할 수 있습니다. 이 방법을 사용할 때는 몇 가지 주의할 점이 있습니다.
- 의도치 않은 트랜잭션 적용으로 프로덕션 환경과 다른 조건에서 테스트될 수 있습니다. 예를 들어,
OSIV
를 꺼두고@Transactional
도 없는 상태에서 지연로딩된 엔티티를 조회하면LazyInitializationException
이 발생하지만, 테스트에서는 트랜잭션이 열려 있어서 예외가 발생하지 않습니다. 따라서 실제로는 실패할 코드가 테스트에서는 성공하는 거짓 음성이 나타날 수 있습니다.
거짓 양성(False Positive): 프로덕션 코드는 정상 동작하지만 테스트는 실패
거짓 음성(False Negative): 프로덕션 코드는 실패하지만 테스트는 통과
@SpringBootTest
의WebEnvironment
가DEFINE_PORT
또는RANDOM_PORT
일 경우, 별도의 스레드에서 서블릿 컨테이너가 실행되기 때문에 테스트의 트랜잭션 롤백이 적용되지 않습니다.- 프로덕션 코드의 트랜잭션 전파 레벨을
REQUIRES_NEW
로 설정했을 경우 새로운 트랜잭션을 생성하기 때문에 테스트 트랜잭션과 무관하여 롤백되지 않습니다. - 비동기 메서드는 새로운 스레드에서 실행되기 때문에 롤백되지 않습니다.