모의 커머스 서비스를 개발하는 과정에서 API에 대한 동작을 확인하기 위해 Testcontainer를 활용하여 테스트를 작성하였는데,
그 과정에서 발생한 내용에 대한 기록이다
회원 모듈을 개발하였고 "회원가입, 회원수정, 회원탈퇴, 로그인, 로그아웃" 이렇게 단순한 기능을 개발하였다
로그인과 로그아웃을 위해 JWT와 Redis를 활용하여 세션을 관리하였다
개발을 마친 후 단위적인 기능에 대한 테스트가 아닌 API를 통해 실제적으로 원하는 결과가 도출되는지 확인하기 위해 통합테스트가 필요하다
개발한 기능에는 Redis라는 서드파티 서비스가 필요하였고, 테스트를 위해 Redis 컨테이너를 그 때마다 띄울 수 없었다
그래서 Testcontainer를 활용하였다
Testcontainer를 활용하면 테스트가 수행되는 동한 테스트용 컨테이너를 자동으로 띄울 수 있기 때문에 통합테스트가 편리해진다
첫번째 회원에 대한 API 테스트를 Testcontainer를 활용하여 작성하고 수행하였다
@Testcontainers
abstract class RedisTestConfig {
companion object {
@JvmStatic
@Container
private val redis = GenericContainer("redis:6.2.7-alpine")
.withExposedPorts(6379)
.withReuse(true)
.apply { start() }
@JvmStatic
@DynamicPropertySource
fun registerRedisProperties(registry: DynamicPropertyRegistry) {
registry.add("spring.data.redis.host") { redis.host }
registry.add("spring.data.redis.port") { redis.getMappedPort(6379).toString() }
}
}
}
Testconatiner를 각 테스트 별로 사용할 수 있도록 abstract 클래스로 작성해둔 것이다
이것을 아래와 같이 상속 받아 테스트를 만들게 되면 테스트가 수행될 때 redis에 대한 테스트 컨테이너가 생성되고 테스트가 수행되게 된다
그러나 이런 방법으로 여러 테스트를 작성하고 테스트를 일괄로 수행하게 되면 문제가 발생하게 된다
이처럼 첫번째 테스트는 잘 수행이 되지만 두번째 테스트에서는 redis container를 찾지 못하여 테스트가 정상적으로 수행되지 않는 모습을 볼 수 있다
해결 방법
이를 해결하는 방법으로는 두가지가 있다
1. redis를 static으로 선언하지 않는다
@Testcontainers
abstract class RedisTestConfig {
companion object {
private val redis = GenericContainer("redis:6.2.7-alpine")
.withExposedPorts(6379)
.withReuse(true)
.apply { start() }
@JvmStatic
@DynamicPropertySource
fun registerRedisProperties(registry: DynamicPropertyRegistry) {
registry.add("spring.data.redis.host") { redis.host }
registry.add("spring.data.redis.port") { redis.getMappedPort(6379).toString() }
}
}
}
2. @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) 를 테스트 클래스 상단에 작성해준다
@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
class AuthControllerV1Test : RedisTestConfig() {
@Autowired
lateinit var memberRepository: MemberJpaRepository
@Autowired
lateinit var redisTemplate: RedisTemplate<String, Any>
.....
}
위 두 방법을 사용하게 되면 앞서 발생한 문제는 해소가 된다
그렇다면 문제가 발생할까?
문제 원인
문제의 원인은 Spring 테스트의 특성 때문에 발생한다
통합테스트를 할 때는 Spring 컨테이너를 통해 필요한 객체를 생성하고 의존성을 주입해야한다
그리고 이를 위해 @SpringBootTest 어노테이션을 붙인다
이를 통해 테스트가 수행될 때 SpringBootTestContextBootstrapper가 동작하면서 ContextLoader를 통해 context(ApplicationContext)를 로드하는 과정에서 캐싱된 context가 있으면 해당 컨텍스트를 재사용하게 된다
- ApplicationContext를 생성하는 비용이 크기 때문에 재사용
@SpringBootTest 동작 흐름
@SpringBootTest
↓
SpringBootTestContextBootstrapper
↓
SpringBootContextLoader.loadContext()
↓
ApplicationContext 생성
↓
의존성 주입
↓
테스트 수행
------------------
@BeforeEach 실행
↓
@Test 실행
↓
@AfterEach 실행
------------------
↓
컨텍스트 캐시 유지 또는 제거
컨텍스트를 재사용하면서 컨텍스트 로드 시점에 동작해야하는 아래 설정 함수가 동작하지 못하고 이로 인해 두번째 테스트에서 redis에 연결을 시도하였으나 실패하면서 테스트가 정상 수행되지 못한 것이다
@JvmStatic
@DynamicPropertySource
fun registerRedisProperties(registry: DynamicPropertyRegistry) {
registry.add("spring.data.redis.host") { redis.host }
registry.add("spring.data.redis.port") { redis.getMappedPort(6379).toString() }
}
회고
이번 문제를 해결하면서 Testcontainer는 물론 통합 테스트를 작성함에 있어서 Spring의 테스트 동작 구조에 대해서 조금 들여다 볼 수 있었다
기본적으로 Spring의 동작 구조에 대해서는 계속적으로 접할 기회가 많고 학습의 기회가 많았는데, Spring의 테스트 메커니즘에 대해서는 다소 학습의 기회가 적지 않았나 하는 생각이 든다