코틀린에서는 아래와 형태와 같은 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}")}
@SpringBootTestinternal 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 // ...})