[Spring/Test] Kotest @Transactional

민찬기·2023년 8월 3일
0

Test

목록 보기
1/2
post-thumbnail

최근 Kotest로 테스트 코드를 작성하면서, 코틀린 특유의 가독성을 즐기며 테스트를 작성하고 있었습니다.

JUnit에서와 마찬가지로 @Transactional을 이용하여 테스트 데이터를 teardown 하는 작업을 진행했습니다.

그런데, 우연치 않게 DB를 확인하다가, 테스트 데이터가 그대로 남아있는 것을 보게 되었습니다.

오늘은 왜 @Transactional이 작동하지 않았는지 살펴보겠습니다.

한 줄 요약

SpringExtension을 사용하자

import io.kotest.extensions.spring.SpringExtension

class ... ({
	extension(SpringExtension)
    
    ...
})

@Transactional

Transaction의 개념이 생소하신 분은 Transaction에 대해 잘 정리된 블로그를 먼저 보시는 것도 좋을 거 같습니다.

우선 간단하게, 테스트 코드에서의 @Transactional에 대해 살펴보겠습니다.

테스트에서 @Transactional은 편의를 위해 사용되곤 합니다. 일반적으로 테스트를 진행할 때, 진행한 데이터들을 모두 rollback 시키기 위해 @Transactional을 사용하곤 합니다.

이는, AfterEach에서 연관관계를 고려하며 삭제 코드를 입력하는 것보다 훨씬 간편합니다. 특히나 테스트 대상이 많은 DB를 건드리는 경우엔 편리하겠죠.

그러나 @Transactional 어노테이션 사용에 주의가 필요하다고 많은 책과 강의에서 얘기하고 있습니다. 이 부분 역시도 다루고자 하는 내용과는 상관이 없기 때문에 블로그 글을 참조하시는 게 좋을 듯 합니다.

@Transactional이 작동을 안 한다!?

JUnit에서 해보자

우선 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에서 해보자

같은 로직으로 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에 남아있는 모습을 볼 수 있습니다.

➕ SpringExtension을 추가하자

import io.kotest.extensions.spring.SpringExtension
...

@ActiveProfiles("test")
@SpringBootTest
@Transactional
class PersonRepositoryKotest(
    private val personRepository: PersonRepository,
) : DescribeSpec({

    extension(SpringExtension)
    
    ...
})

SpringExtension을 추가하면 @Transactional이 정상적으로 동작하는 것을 볼 수 있습니다.

SpringExtension은 SpringBootTest 안에 있는데? (추측)

@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 생명주기 및 트랜잭션

profile
https://github.com/devmizz

2개의 댓글

comment-user-thumbnail
2023년 8월 3일

정보 감사합니다.

답글 달기
comment-user-thumbnail
2024년 7월 16일

Kotest에서는 io.kotest.extensions.spring.SpringExtension 이 동작하기에, @SpringBootTest에 붙은 org.springframework.test.context.junit.jupiter.SpringExtension은 동작하지 않는 것이 아닐까요?

답글 달기