Spring Boot Test 데이터 초기화 문제

kshired·2024년 1월 22일
1

같은 Application Context를 공유하게 되면, Spring Boot Test에서는 같은 DB를 공유하게 되는 문제가 있다.

물론 @Transactional 이라는 간편하면서도 악마같은 도구를 사용하면, 매 테스트가 끝날 때 자동으로 데이터를 클렌징해주니 문제가 없겠지만 @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를 상속하여 테스트를 작성하면, 매 테스트가 실행될 때마다 beforeTestMethodafterTestMethod에 의해 데이터가 초기화된다.

profile
글 쓰는 개발자

0개의 댓글