Skip to content

Kotest

코틀린에서는 아래와 형태와 같은 DSL(Domain Specific Language) 스타일의 중괄호를 활용한 코드 스타일을 제공한다. 코틀린 내부에서 제공하는 Standard library 대부분도 DSL을 이용해 작성된 것을 볼 수 있다.

하지만 기존에 사용하던 Junit과 AssertJ, Mockito를 사용하면 Mocking이나 Assertion 과정에서 코틀린 DSL 을 활용할 수 없다.

기존에 JAVA에서 사용하던 Junit, Assertion, Mockito 등의 테스트 프레임워크 대신에, Kotest나 Mockk와 같은 도구들을 사용하면 아래처럼 코틀린 DSL과 Infix를 사용해 코틀린 스타일의 테스트 코드를 작성할 수 있다.


Kotest

Kotest는 코틀린 진영에서 가장 많이 사용되는 테스트 프레임워크아다. 코틀린 DSL을 활용해 테스트 코드를 작성할 수 있으며 아래와 같은 기능들을 포함하고 있다.

  • 다양한 테스트 레이아웃(String Spec, Describe Spec, Behavior Spec 등) 제공
  • Kotlin DSL 스타일의 Assertion 기능 제공
  • 데이터 기반 테스트로 많은 양의 매개변수도 테스트 가능
  • 모든 테스트에 대해 호출수, 병렬 처리, 시간제한, 테스트 그룹화, 조건부 비활성 등의 미세 조정 테스트 가능
  • 중첩 테스트기능 제공
  • 동적 테스트 제공 (런타임에 조건부로 테스트를 추가 가능)
  • 테스트 수명주기에 맞는 다양한 콜백을 제공

Kotest를 사용하기 위해서는 아래와 같은 설정 / 의존성 추가가 필요하다.

test {
useJUnitPlatform()
}
dependencies {
testImplementation("io.kotest:kotest-runner-junit5:${Versions.KOTEST}")
testImplementation("io.kotest:kotest-assertions-core:${Versions.KOTEST}")
}

Kotest는 테스트를 위한 많은 레이아웃을 제공한다.

  • Annotation Spec
  • Behavior Spec
  • Describe Spec
  • Fun Spec

모든 Spec에 대한 정보는 여기서 확인해보자.

Kotest Annotation Spec

기존 Junit 방식과 가장 유사한 방식이다. 별 다른 장점이 없는 레이아웃이지만 Junit에서 Kotest로의 마이그레이션이 필요한 상황이라면 나쁘지 않은 선택이 될 수 있다.

internal class CalculatorAnnotationSpec: AnnotationSpec() {
private val sut = Calculator()
@Test
fun `1과 2를 더하면 3이 반환된다`() {
val result = sut.calculate("1 + 2")
result shouldBe 3
}
@Test
fun `식을 입력하면, 해당하는 결과값이 반환된다`() {
calculations.forAll { (expression, answer) ->
val result = sut.calculate(expression)
result shouldBe answer
}
}
@Test
fun `입력값이 null 이거나 빈 공백 문자일 경우 IllegalArgumentException 예외를 던진다`() {
blanks.forAll {
shouldThrow<IllegalArgumentException> {
sut.calculate(it)
}
}
}
@Test
fun `사칙연산 기호 이외에 다른 문자가 연산자로 들어오는 경우 IllegalArgumentException 예외를 던진다 `() {
invalidInputs.forAll {
shouldThrow<IllegalArgumentException> {
sut.calculate(it)
}
}
}
companion object {
private val calculations = listOf(
"1 + 3 * 5" to 20.0,
"2 - 8 / 3 - 3" to -5.0,
"1 + 2 + 3 + 4 + 5" to 15.0
)
private val blanks = listOf("", " ", " ")
private val invalidInputs = listOf("1 & 2", "1 + 5 % 1")
}
}

Kotest Behavior Spec

기존 스프링 기반 프로젝트에서 작성하던 Given, When, Then 패턴을 Kotest Behavior Spec을 활용해 간결하게 정의할 수 있다.

internal class CalculatorBehaviorSpec : BehaviorSpec({
val sut = Calculator()
given("calculate") {
val expression = "1 + 2"
`when`("1과 2를 더하면") {
val result = sut.calculate(expression)
then("3이 반환된다") {
result shouldBe 3
}
}
`when`("수식을 입력하면") {
then("해당하는 결과값이 반환된다") {
calculations.forAll { (expression, answer) ->
val result = sut.calculate(expression)
result shouldBe answer
}
}
}
`when`("입력값이 null이거나 빈 값인 경우") {
then("IllegalArgumentException 예외를 던진다") {
blanks.forAll {
shouldThrow<IllegalArgumentException> {
sut.calculate(it)
}
}
}
}
`when`("사칙연산 기호 이외에 다른 연산자가 들어오는 경우") {
then("IllegalArgumentException 예외를 던진다") {
invalidInputs.forAll {
shouldThrow<IllegalArgumentException> {
sut.calculate(it)
}
}
}
}
}
}) {
companion object {
private val calculations = listOf(
"1 + 3 * 5" to 20.0,
"2 - 8 / 3 - 3" to -5.0,
"1 + 2 + 3 + 4 + 5" to 15.0
)
private val blanks = listOf("", " ", " ")
private val invalidInputs = listOf("1 & 2", "1 + 5 % 1")
}
}

Kotest Describe Spec

Kotest는 Describe Spec을 통해 DCI(Describe, Context, It) 패턴 형태의 레이아웃도 제공한다.

internal class CalculatorDescribeSpec : DescribeSpec({
val sut = Calculator()
describe("calculate") {
context("식이 주어지면") {
it("해당 식에 대한 결과값이 반환된다") {
calculations.forAll { (expression, data) ->
val result = sut.calculate(expression)
result shouldBe data
}
}
}
context("0으로 나누는 경우") {
it("Infinity를 반환한다") {
val result = sut.calculate("1 / 0")
result shouldBe Double.POSITIVE_INFINITY
}
}
context("입력값이 null이거나 공백인 경우") {
it("IllegalArgumentException 예외를 던진다") {
blanks.forAll {
shouldThrow<IllegalArgumentException> {
sut.calculate(it)
}
}
}
}
context("사칙연산 기호 이외에 다른 문자가 연산자로 들어오는 경우") {
it("IllegalArgumentException 예외를 던진다") {
invalidInputs.forAll {
shouldThrow<IllegalArgumentException> {
sut.calculate(it)
}
}
}
}
}
}) {
companion object {
val calculations = listOf(
"1 + 3 * 5" to 20.0,
"2 - 8 / 3 - 3" to -5.0,
"1 + 2 + 3 + 4 + 5" to 15.0
)
val blanks = listOf("", " ", " ")
val invalidInputs = listOf("1 & 2", "1 + 5 % 1")
}
}

위와 같은 여러 레이아웃 중 프로젝트 상황에 가장 잘 맞는 레이아웃을 골라 사용하면 된다.

상황에 따라 kotest 플러그인을 깔아야 test 실행 버튼이 나타날 수도 있다.


Kotest with @SpringBootTest

@SpringBootTest와 같은 통합 테스트에서도 Kotest의 테스트 레이아웃을 사용할 수 있다.

사용을 위해서는 아래와 같은 spring extension 의존성의 추가가 필요하다.

dependencies {
testImplementation("io.kotest:kotest-extensions-spring:${Versions.KOTEST}")
}
@SpringBootTest
internal class CalculatorSpringBootSpec : DescribeSpec() {
override fun extensions() = listOf(SpringExtension)
@Autowired
private lateinit var calculatorService: CalculatorService
init {
this.describe("calculate") {
context("식이 주어지면") {
it("해당 식에 대한 결과값이 반환된다") {
calculations.forAll { (expression, data) ->
val result = calculatorService.calculate(expression)
result shouldBe data
}
}
}
}
}
companion object {
private val calculations = listOf(
"1 + 3 * 5" to 20.0,
"2 - 8 / 3 - 3" to -5.0,
"1 + 2 + 3 + 4 + 5" to 15.0
)
}
}

Kotest Isolation Mode

Kotest는 테스트 간 격리에 대한 설정을 제공하고 있다.

  • SingleInstance – Default
  • InstancePerTest
  • InstancePerLeaf

Kotest에서는 테스트 간 격리 레벨에 대해 디폴트로 SingleInstance를 설정하고 있는데, 이 경우 Mocking 등의 이유로 테스트 간 충돌이 발생할 수 있다. 따라서 테스트간 완전한 격리를 위해서는 아래와 같이 IsolationMode를 InstancePerLeaf로 지정해 사용해야 한다.

internal class CalculatorDescribeSpec : DescribeSpec({
isolationMode = IsolationMode.InstancePerLeaf
// ...
})