스프링 테스트의 경우 컨텍스트 로딩으로 많은 시간을 잡아 먹는다.
잦은 컨텍스트 로딩은 테스트 코드 실행 시간을 많이 잡아먹는 주범이기도 한다.
스프링은 각 테스트 케이스에서 사용하는 컨텍스트 설정이 같다면 이전에 로드했던 컨텍스트를 재사용하는 캐싱 기능이 존재한다.
별도의 옵션 설정이 필요한 것은 아니고 컨텍스트 설정만 같다면 자동으로 재사용한다.
[컨텍스트가 리로딩 되는 케이스]
@WebMvcTest( value = [ MemberFindController::class ])
class MemberFindControllerTest : BaseMockMvcTest() {
@MockBean
lateinit var memberFindReadCase: MemberFindReadCase
companion object {
const val PREFIX = "/public/member"
const val FIND_EMAIL_URL = "$PREFIX/findEmail"
const val FIND_PASSWD_URL = "$PREFIX/findPassword"
}
...
}
@WebMvcTest( value = [ MemberPrivateWriteController::class ])
class MemberPrivateWriteControllerTest : BaseMockMvcTest() {
companion object {
const val UPDATE_PHONE_URL = "/member/phone"
}
...
}
위의 두 컨트롤러 테스트를 보면 WebMvcTest에 value로 입력되는 컨트롤러가 다르고,
첫번째 테스트의 경우 MockBean 설정이 존재하지만 두번째 테스트의 경우 MockBean 설정이 없다.
두 테스트에서 사용하는 빈 설정이 다르다. 이로인해 각 테스트에서 컨텍스트 로딩이 발생된다.
스프링 컨텍스트 캐싱을 사용하기 위해서 사소한 목 설정 하나 조차도 모두 동일하게 맞춰야 한다.
나의 경우 컨트롤러 모듈과 리포지토리 모듈에서 컨텍스트 로딩이 필요했는데,
각 모듈 내에서 모든 TC들이 하나의 테스트 환경을 공유하도록 설정하여 컨텍스트 로딩 횟수를 1번만 일어나도록 하였다.
[WebMvcTest 환경 구성] - Controller 모듈
1. 의존객체 모킹 설정
[ 3rd-party-lib 빈 모킹]
class SecurityWebMockBeanConfig {
@Bean
fun securityConfig(): SecurityConfig = SecurityConfig.newInstance()
@Bean
fun securityFilterChain(): SecurityFilterChain = object : SecurityFilterChain {
override fun matches(request: HttpServletRequest?): Boolean {
return true
}
override fun getFilters(): MutableList<Filter> {
return mutableListOf()
}
}
@Bean
fun handlerMethodArgumentResolver(): HandlerMethodArgumentResolver {
return MockUserDetailArgumentResolver()
}
@Bean
fun webConfig(): WebConfig {
return WebConfig(handlerMethodArgumentResolver())
}
}
[서비스 레이어 빈 모킹]
class MathDocsMockBeanConfig {
@Bean
fun mathDocsPaperReadCase(): MathDocsPaperReadCase = MockMathDocsPaperReadCase()
@Bean
fun mathDocsPaperWriteCase(): MathDocsPaperWriteCase = MockMathDocsPaperWriteCase()
@Bean
fun mathDocsReadCase(): MathDocsReadCase = MockMathDocsReadCase()
@Bean
fun mathDocsUsageWriteCase(): MathDocsUsageWriteCase = MockMathDocsUsageWriteCase()
}
[모킹 빈 설정 통합]
@Import(
SecurityWebMockBeanConfig::class,
MathDocsMockBeanConfig::class,
. . .
)
@TestConfiguration
class RestApiWebMvcMockBeanConfig
컨트롤러는 서비스 레이어의 객체들을 의존하고 있다.
스프링 시큐리티 모듈과 같은 3rd-party 의존성 또한 존재한다.
컨트롤러 레이어에서는 컨트롤러만 테스트할 것이므로 서비스 객체와 외부 의존성 객체를 모두 모킹하였다.
이후 모킹 빈 설정을 하나의 TestConfiguration으로 통합하였다.
(모킹 빈 config 파일을 분리한 기준은 패키지 단위로 진행하였다. 각자 알아서 편한 기준으로 정하면 될 것 같다.)
2. 테스트 config 어노테이션 작성
@ContextConfiguration(classes = [RestApiWebMvcMockBeanConfig::class])
@WebMvcTest(
value = [
LoginFailureController::class,
MemberReadController::class,
MemberWriteController::class,
...
]
)
@ActiveProfiles("rest-api")
annotation class WebMvcUnitTest
이제 컨트롤러 테스트 환경 구성 어노테이션을 작성하면 된다.
WebMvcTest의 value에 테스트를 진행할 컨트롤러를 넣어주자.
해당 value에 등록이 되어야 mockMvc 테스트를 진행할 수 있다.
또한 value에 등록되는 컨트롤러의 의존객체 빈 설정 또한 정상적으로 되어있어야 컨텍스트 구동이 완료될 수 있다.
우리는 이전에 의존객체를 모킹한 빈 설정을 만들어 두었으니 해당 설정을 ContextConfiguration에 넣어주자.
컨텍스트가 로딩되면 실제 프로덕션의 빈이 아닌 모킹 빈들이 스프링 빈으로 등록될 것이다.
profile 설정이 필요하다면 프로필 설정까지 진행하면 된다.
우리가 설정한 테스트 config에 대해 설명하면 테스트 대상인 controller만 실제 production 객체로 등록되고
그 외 컨트롤러가 의존하는 서비스 객체와 시큐리티와 같은 외부 의존성에 의해 등록이 필요한 빈들은
직접 작성한 모킹 빈으로 대체하였다.
따라서 직접 이러한 환경을 구성할 때, 테스트 대상이 아닌 의존 객체들에 대한 모킹 처리를 모두 진행하여 환경 구성을 해야한다는 것에 주의하자.
자 이제, 아래와 같이 우리가 만든 어노테이션을 테스트 코드에 붙여 테스트 실행을 확인해보자.
정상 동작을 확인할 수 있을 것이고 해당 어노테이션을 붙인 모든 테스트에서는 컨텍스트 환경이 같기에 컨텍스트를 최초 1번만 구동하고 공유하여 사용할 것이다.
@WebMvcUnitTest
class MemberFindControllerTest : BaseMockMvcTest() {
companion object {
const val PREFIX = "/public/member"
const val FIND_EMAIL_URL = "$PREFIX/findEmail"
const val FIND_PASSWD_URL = "$PREFIX/findPassword"
}
@Test
fun `이메일 찾기 - 성공`() {
// given
val reqBody = mapOf(
"userName" to "이름",
"phoneNumber" to "01012341234"
)
//when
val resultAction = getRequest(FIND_EMAIL_URL, reqBody)
// then
assert2xx(resultAction)
}
[DataJpaTest 환경 구성] - Repository 모듈
1. TestContainer 설정
2. DataSource 설정
3. 의존객체 모킹
4. 테스트 config 어노테이션 설정
1. TestContainer 설정
나의 경우 TestContainer를 통해 테스트 DB 환경을 구축하여 아래와 같이 설정하였다.
TestContainer를 소개하는 글은 이부분에 대한 자세한 소개는 건너뛰고,
자신의 테스트 DB 환경에 따른 설정을 진행해주자.
class MysqlTCExtension : Extension {
companion object {
var mysqlContainer: MySQLContainer<Nothing> =
MySQLContainer<Nothing>("mysql:8.3.0")
.apply {
this.withDatabaseName("numberbox_tc")
this.withUsername("root")
this.withPassword("1111")
this.withUrlParam("characterEncoding", "UTF-8")
}
init {
mysqlContainer.start()
}
}
}
2. DataSource 설정
나의 테스트 DB 환경인 TestContainer로 커넥션이 연결되도록 설정하였다.
@TestConfiguration
class TCDataSourceConfig {
@Bean
fun dataSource(): HikariDataSource {
return DataSourceBuilder.create()
.type(HikariDataSource::class.java)
.url(MysqlTCExtension.mysqlContainer.jdbcUrl)
.username(MysqlTCExtension.mysqlContainer.username)
.password(MysqlTCExtension.mysqlContainer.password)
.build()
}
}
3. 외부 의존성 모킹
리포지토리는 mvc 레이어의 마지막 레이어에 위치하여 의존객체가 없는 경우가 많다.
나의 경우에도 아래와 같이 외부 의존 모듈에 의한 설정만 진행했을 뿐 이외에는 존재하지 않았다.
이전에 컨트롤러에서 모킹 처리 진행했던 것과 같은 절차이다.
@TestConfiguration
class MockOrmBeanConfig {
@Bean
fun ipAddressService(): IPAddressService = object : IPAddressService {
override fun getIPAddress(): String {
return "127.0.0.1"
}
override fun getPublicIPAddress(): String {
return "127.0.0.1"
}
}
}
4. 테스트 config 설정
@DataJpaTest
@ExtendWith(value = [MysqlTCExtension::class])
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import(value = [QueryDslConfiguration::class, MockOrmBeanConfig::class, TCDataSourceConfig::class])
@ActiveProfiles("orm-jpa-adapter-tc-test")
annotation class TcDBJpaTest
이전 1~3번에 설정했던 config를 한군데에 모아놓았다.
추가된 것이 있다면 QueryDsl을 사용하므로 production의 querydsl config를 Import 해두었다.
DB단 테스트이게 DB설정과 커넥션 설정이 추가됬을 뿐, 의존객체를 모킹하고 테스트 config를 설정한 컨트롤러 모듈의 테스트 환경 구성 절차와 같다.
마찬가지로 사용하는 것에서 테스트용 Config 어노테이션만 붙여준다면 해당 어노테이션을 사용하는 모든 곳에서 컨텍스트 캐싱을 통해 공유하게 할 수 있다.
@TcDBJpaTest
class MathContentsGrammarWriteRepositoryTest(
@Autowired
private val em: EntityManager,
@Autowired
private val mathContentsGrammarWriteRepository: MathContentsGrammarWriteRepository
) {
@Test
fun `수학문제 문법 저장`() {
// given
val contentsId = 4907L
val grammar = ""
// when
mathContentsGrammarWriteRepository.createGrammar(contentsId, grammar)
em.flush()
em.clear()
}
정리
WebMvcTest와 DataJpaTest를 사용하는 테스트에서는 환경 구성을 하는 내용을 소개하였지만,
SpringBootTest는 소개하지 않았다.
소개할 수 없었던 이유는 SpringBootTest는 작성된 케이스가 하나도 존재하지 않았다.
웹서버와 DB와 같은 외부 소프트웨어 연동이 필요로 한 경우에는 스프링 프레임워크를 사용하였지만
그 외의 테스트에서는 스프링 프레임워크 없이 순수 프로그래밍 언어로 작성하였다.
컨텍스트 캐싱은 매우 유용한 기능이다.
컨텍스트 캐싱을 적극적으로 이용하는 것도 중요하지만, 이것보다 우선 고려해야하는 것은 컨텍스트를 로딩하지 않는 것이라고 생각한다. 전체 프로젝트에 스프링 컨텍스트를 로딩하는 테스트가 몇개 존재하였지만 이것들도 모두 제거를 하고 결국에는 WebMvcTest와 DataJpaTest만 남게 되었다.
'테스트' 카테고리의 다른 글
flyway와 testContainer를 통한 멱등성을 지키는 DB 환경 구성 (0) | 2025.02.22 |
---|---|
테스트 더블을 직접 구현하여 테스트 환경 구축하기(feat. 동시성 제어) (0) | 2025.02.21 |
테스트 커버리지 100% 달성기[3] - 테스트 코드 가독성 개선 (0) | 2025.02.19 |
테스트 커버리지 100% 달성기[2] - 테스트 시간 단축 (0) | 2025.01.27 |
테스트 커버리지 100% 달성기[1] - 레이어별 100% 달성 과정 (2) | 2025.01.04 |