같은 Application Context를 공유하게 되면, Spring Boot Test에서는 같은 DB를 공유하게 되는 문제가 있다.
물론 @Transactional
이라는 간편하면서도 악마같은 도구를 사용하면, 매 테스트가 끝날 때 자동으로 데이터를 클렌징해주니 문제가 없겠지만 @Transactional
을 붙이게 되면 테스트 코드와 실 동작하는 코드의 로직이 달라지는 문제가 발생한다.
Spring Boot Test에서 Transactional을 사용하지 않는다면, 하나의 ApplicationContext에서 뜨는 테스트들은 같은 테스용 DB를 공유하여 방식으로 작동할 것이다.
이렇게 된다면, 각각의 테스트에서 사용하던 데이터들이 다른 테스트 데이터들에 영향을 줄 수 있는 상황이 발생하게 된다.
즉, Transactional을 사용했을 때 고민하지 않아도 될 데이터 초기화 및 삭제에 대한 고민을 해야하는 상황이 온 것이다.
이걸 어떻게 해결할 수 있을까?
Spring에서 제공하는 AbstractTestExecutionListener
를 상속하여 TestExecutionListener를 구현하고, 그 Listener를 테스트에서 동작하게 하면 된다.
class TestExecutionListener : AbstractTestExecutionListener() {
override fun getOrder(): Int {
return Ordered.LOWEST_PRECEDENCE - 100
}
// 이 부분은 선택사항이다. 기본적으로 세팅되어야하는 데이터가 있다면, 이런식으로 초기화할 수 있다.
override fun beforeTestMethod(testContext: TestContext) {
val entityManager = testContext.getEntityManager()
entityManager.runWith {
getDataSql().forEach { sql ->
it.createNativeQuery(sql).executeUpdate()
}
}
}
override fun afterTestMethod(testContext: TestContext) {
val entityManager = testContext.getEntityManager()
entityManager.runWith {
getTruncateSql(it).forEach { sql ->
it.createNativeQuery(sql).executeUpdate()
}
}
}
private fun TestContext.getEntityManager(): EntityManager {
val emf = this.applicationContext.getBean(EntityManagerFactory::class.java)
return emf.createEntityManager()
}
private fun EntityManager.runWith(fn: (EntityManager) -> Unit) {
this.transaction.begin()
runCatching {
fn(this)
this.transaction.commit()
}.onFailure {
this.transaction.rollback()
}
this.close()
}
private fun getDataSql(): List<String> {
val scrip = ClassPathResource("data.sql")
return scrip.inputStream.bufferedReader().readLines().filter { it.isNotBlank() }
}
private fun getTruncateSql(entityManager: EntityManager): List<String> {
return entityManager.createNativeQuery(
"""
SELECT CONCAT('TRUNCATE TABLE ', TABLE_NAME, ';') AS query
FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'public'
""".trimMargin(),
).resultList.map { it as String }
}
}
이렇게 구현한 TestExecutionListener
는 TestExecutionListeners를 통해 사용할 수 있다.
@SpringBootTest
@TestExecutionListeners(
value = [TestExecutionListener::class],
mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
)
class BaseTest
이제 BaseTest를 상속하여 테스트를 작성하면, 매 테스트가 실행될 때마다 beforeTestMethod
와 afterTestMethod
에 의해 데이터가 초기화된다.