스프링에서 코틀린을 사용할 때, 테스트 도구로 JUnit뿐만 아니라 kotest를 사용할 수 있다.
kotest는 다양한 테스트 스타일을 제공한다. JUnit과 유사한 Annotation Spec, String Spec, 그리고 우리가 흔히 사용하는 given when then 스타일을 사용할 수 있도록 Behaivor Spec을 제공해주기도 한다. DCI 패턴을 활용할 수 있도록 Describe Spec도 제공해준다. 이번 시간에는 kotest에서 제공해주는 spec을 살펴볼 것이다.
kotest의 장점
- JUnit, Mockito를 사용할 경우 Kotlin DSL을 사용하지 못한다는 단점이 존재한다. kotest를 활용하면 kotlin DSL을 사용해 더 코틀린스러운 테스트를 작성할 수 있게 된다.
위 장점은 가독성 증가, 중복 제거 등 추가적인 장점도 같이 챙길 수 있게 해준다.
kotest 사용해보기
개발 환경
- Spring Boot 2.3.3
- kotlinVersion 1.6.21
- gradle
- IntelliJ
build.gradle.kts
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
dependencies {
testImplementation("io.kotest:kotest-runner-junit5:5.3.2")
testImplementation("io.kotest.extensions:kotest-extensions-spring:1.1.1")
}
설치할 플러그인
인텔리제이 플러그인으로 kotest를 설치해주어야 한다.
설치하지 않으면 현재 (22.08.13) 기준으론 위와 같이 StringSpec 등 일부 스펙에서 프레임워크가 인식하지 못해 테스트를 진행할 수 없을 수 있다.
AnnotationSpec
JUnit과 가장 유사한 테스트 스타일이다. JUnit -> kotest로 마이그레이션할 때 가장 변경이 적은 테스트 스타일이긴 하다.
그렇지만 NON-ASCII 경고가 발생하기도 하고, StringSpec이라는 대체제도 있으므로 개인적으론 StringSpec을 쓰는 게 더 나아보인다.
import io.kotest.core.spec.style.AnnotationSpec
class UserTest : AnnotationSpec() {
@Test
fun `회원의 비밀번호와 일치하는지 확인한다()`{
val user = createUser()
shouldNotThrowAny { user.authenticate(PASSWORD) }
}
@Test
fun `회원의 비밀번호와 다를 경우 예외가 발생한다`() {
val user = createUser()
shouldThrow<UnidentifiedUserException> { user.authenticate(WRONG_PASSWORD) }
}
}
StringSpec
@Test라는 어노테이션을 붙이지 않아도 되고, fun 키워드 없이 바로 테스트명을 String으로 지을 수 있다는 점에서 매력적이라 느껴진다. 한글로 작성해도 인텔리제이에서 노란줄을 띄워주지 않아 편안하다.
StringSpec은 AnnotationSpec과 같이 괄호 위치가 ({ ... })인 점에 유의하자.
import io.kotest.core.spec.style.StringSpec
class UserTest : StringSpec({
"회원의 비밀번호와 일치하는지 확인한다" {
val user = createUser()
shouldNotThrowAny { user.authenticate(PASSWORD) }
}
"회원의 비밀번호와 다를 경우 예외가 발생한다" {
val user = createUser()
shouldThrow<UnidentifiedUserException> { user.authenticate(WRONG_PASSWORD) }
}
})
BehaviorSpec
Given-When-Then 테스트 패턴을 쓰고 싶을 때 사용한다.
import io.kotest.core.spec.style.BehaviorSpec
internal class UserServiceTest : BehaviorSpec({
val userRepository: UserRepository = mockk()
val passwordGenerator: PasswordGenerator = mockk()
Given("유저의 비밀번호가 주어질 때") {
When("비밀번호를 변경하려 하면") {
var request: EditPasswordRequest = mockk()
slot<Long>().also { slot ->
every { userRepository.getById(capture(slot)) } answers { createUser(id = slot.captured) }
}
Then("확인용 비밀번호가 일치한다면 변경한다") {
// ...
}
Then("확인용 비밀번호가 일치하지 않으면 예외가 발생한다") {
// ...
}
}
}
})
Given When Then을 소문자로 작성하지 않도록 주의하자.
소문자로 작성할 경우 given, then은 상관없지만 when은 백틱을 쳐주도록 하자. (given `when` then)
참고로 slot은 mockK 관련 키워드이다. mockito 대신 mockK를 이용하면 더 코틀린스러운 테스트를 작성 가능하다.
DescribeSpec
DCI 패턴을 쓰고 싶을 때 사용한다.
import io.kotest.core.spec.style.DescribeSpec
internal class UserServiceTest : DescribeSpec({
val userRepository: UserRepository = mockk()
describe("UserService") {
var user: User = createUser()
var request: ResetPasswordRequest = mockk()
context("비밀번호를 비교할 때") {
var request: EditPasswordRequest = mockk()
slot<Long>().also { slot ->
every { userRepository.getById(capture(slot)) } answers { createUser(id = slot.captured) }
}
// ...
it("확인용 비밀번호가 일치한다면 변경한다") {
// ...
}
it("확인용 비밀번호가 일치하지 않으면 예외가 발생한다") {
// ...
}
}
}
})
Given When Then 패턴이 아닌 DCI 패턴을 사용하고 싶을 경우 DescribeSpec을 사용하면 된다.
BehaviorSpec의 Given-When-Then 패턴과 DescribeSpec의 DCI 패턴은 테스트 중첩 (Given 안에 When 여러 개, When 안에 Then 여러 개처럼)이 가능하다는 장점이 있다. 이는 중복을 제거하기에 좋다.
(하지만 개인적으론 테스트 메서드명을 짜는 데에 많은 고민을 해야 되고, 이후 테스트 코드 변경이 일어날 때 변경하는 데에 더 많은 시간이 걸려 더 좋다고 생각하지는 않는다. 아직 내가 Kotest 초보 개발자여서 그런 것일수도 있지만 말이다.)
더 다양한 kotest testing styles를 참고하고 싶다면 아래 글을 보자.
https://kotest.io/docs/framework/testing-styles.html
참고
도움을 준 사람
우아한테크코스 4기 BE 제로(https://github.com/asebn1)
우아한테크코스 4기 BE 조조그린(https://github.com/jojogreen91)
'Kotlin > Kotlin | Spring 학습기록' 카테고리의 다른 글
[Kotlin] kotlin은 왜 Java와 달리 Checked Exception을 제공하지 않을까? (4) | 2022.11.15 |
---|---|
[Kotest] Nested Test spec에서의 context 생명주기 및 트랜잭션 (11) | 2022.09.08 |
[Kotlin] Kotlin DSL + Spring REST Docs + MockMvc 적용기 (2) (0) | 2022.07.17 |
[Kotlin] Kotlin DSL + Spring REST Docs + MockMvc 적용기 (1) (2) | 2022.07.17 |
[Kotlin] Sealed Class를 이용한 무분별한 상속 확장을 방지하기 (0) | 2022.06.04 |