최근 Kotest로 테스트 코드를 작성하면서, 코틀린 특유의 가독성을 즐기며 테스트를 작성하고 있었습니다.
JUnit에서와 마찬가지로 @Transactional
을 이용하여 테스트 데이터를 teardown 하는 작업을 진행했습니다.
그런데, 우연치 않게 DB를 확인하다가, 테스트 데이터가 그대로 남아있는 것을 보게 되었습니다.
오늘은 왜 @Transactional
이 작동하지 않았는지 살펴보겠습니다.
SpringExtension
을 사용하자
import io.kotest.extensions.spring.SpringExtension
class ... ({
extension(SpringExtension)
...
})
Transaction의 개념이 생소하신 분은 Transaction에 대해 잘 정리된 블로그를 먼저 보시는 것도 좋을 거 같습니다.
우선 간단하게, 테스트 코드에서의 @Transactional
에 대해 살펴보겠습니다.
테스트에서 @Transactional
은 편의를 위해 사용되곤 합니다. 일반적으로 테스트를 진행할 때, 진행한 데이터들을 모두 rollback 시키기 위해 @Transactional
을 사용하곤 합니다.
이는, AfterEach에서 연관관계를 고려하며 삭제 코드를 입력하는 것보다 훨씬 간편합니다. 특히나 테스트 대상이 많은 DB를 건드리는 경우엔 편리하겠죠.
그러나 @Transactional
어노테이션 사용에 주의가 필요하다고 많은 책과 강의에서 얘기하고 있습니다. 이 부분 역시도 다루고자 하는 내용과는 상관이 없기 때문에 블로그 글을 참조하시는 게 좋을 듯 합니다.
우선 JUnit에서 @Transactional
을 붙인 체로 테스트를 진행해보겠습니다.
@ActiveProfiles("test")
@SpringBootTest
@Transactional
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class PersonRepositoryJunitTest @Autowired constructor(
private val personRepository: PersonRepository
) {
@BeforeAll
fun setUp() {
println("==============================")
}
@AfterAll
fun printSizeAfterTest() {
println("테스트 종료 후 데이터 크기: ${personRepository.findAll().size}")
println("==============================")
}
@Test
@DisplayName("Person을 저장하면, 저장된 객체가 반환된다.")
fun createPerson() {
// given
val person = Person(null, "안유진", 19)
// when
personRepository.save(person)
// then
println("테스트 종료 전 데이터 크기: ${personRepository.findAll().size}")
}
}
@Transactional
이 정상적으로 작동하여, 테스트에서 생성된 데이터가 삭제된 모습을 볼 수 있습니다.
같은 로직으로 Kotest의 Describe Spec에서 진행을 해보겠습니다.
@ActiveProfiles("test")
@SpringBootTest
@Transactional
class PersonRepositoryKotest @Autowired constructor(
private val personRepository: PersonRepository,
) : DescribeSpec({
beforeSpec {
println("==============================")
}
afterSpec {
println("테스트 종료 후 데이터 크기: ${personRepository.findAll().size}")
println("==============================")
}
describe("PersonService") {
context("저장을 하는 경우") {
val person = Person(null, "안유진", 19)
it("저장된 객체를 반환한다.") {
personRepository.save(person)
println("테스트 종료 전 데이터 크기: ${personRepository.findAll().size}")
}
}
}
})
테스트에서 생성된 Person 데이터가 아직 DB에 남아있는 모습을 볼 수 있습니다.
import io.kotest.extensions.spring.SpringExtension
...
@ActiveProfiles("test")
@SpringBootTest
@Transactional
class PersonRepositoryKotest(
private val personRepository: PersonRepository,
) : DescribeSpec({
extension(SpringExtension)
...
})
SpringExtension
을 추가하면 @Transactional
이 정상적으로 동작하는 것을 볼 수 있습니다.
@SpringBootTest
어노테이션 안에는 @ExtendWith(SpringExtension.class)
가 있습니다. 그러나 어떤 이유에서인지 @SpringBootTest
어노테이션 내부의 SpringExtension이 동작하지 않았고, Test LifeCycle을 관리하는 SpringExtension이 동작하지 않으면서, @Transactional
도 동작하지 않는 상황이 생기지 않았을까 하는 추측을 합니다.
아직도 정확한 이유는 모르겠습니다. (진짜 검색해도 안 나오는데, 답을 아시는 분은 댓글 좀 부탁드리겠습니다!)
Kotest의 Transaction 단위는 leaf
로 구성됩니다.
Kotest의 leaf란 각 Test Spec에서 제일 깊은 depth를 칭합니다.
(ex. Describe Spec의 'it', Behavior Spec의 'then')
따라서 아래와 같이 it 밖에서 저장을 진행하더라도, it 밖에서 진행한 영속성 작업은 롤백되지 않습니다.
@ActiveProfiles("test")
@SpringBootTest
@Transactional
class PersonRepositoryKotest(
private val personRepository: PersonRepository,
) : DescribeSpec({
extension(SpringExtension)
describe("PersonService") {
context("저장을 하는 경우") {
println("=== BEFORE IT ===")
personRepository.save(Person(null, "안유진", 19))
println(personRepository.findAll())
it("저장된 객체를 반환한다.") {
println("=== SAVE IN IT ===")
personRepository.save(Person(null, "장원영", 18))
println(personRepository.findAll())
}
println("=== AFTER IT ===")
println(personRepository.findAll())
}
}
})
it 구문 안에서 실행된 데이터만 롤백된 것을 확인할 수 있습니다.
해당 설정은 변경이 가능한데, 해당 블로그를 참조하면 가능합니다.
[Spring] 트랜잭션에 대한 이해와 Spring이 제공하는 Transaction(트랜잭션) 핵심 기술 - (1/3)
JPA 사용시 테스트 코드에서 @Transactional 주의하기
[Kotest] Nested Test spec에서의 context 생명주기 및 트랜잭션
정보 감사합니다.