목차

들어가기 전에
보통 자바 + 스프링 환경에서 테스트 코드를 작성할 때는 JUnit을 많이 사용합니다. 스프링부트에서 기본적으로 제공하는 테스트 프레임워크가 JUnit이기도 하고, 오랫동안 사용되다 보니 레퍼런스도 풍부해서 자연스럽게 JUnit을 사용해서 테스트 코드를 작성하는 경우가 많죠.
코틀린을 사용할 때는 코틀린에 특화된 테스트 프레임워크인 Kotest를 사용할 수도 있습니다. Kotest는 더 직관적이고 코틀린스러운 테스트 코드를 작성할 수 있다는 장점이 있죠. 이번 글에서는 Kotest에 대해 정리하겠습니다.
Kotest?
Kotest는 JUnit을 환경에서 실행되는, 코틀린에 특화된 테스트 프레임워크입니다. 코틀린 전용의 JUnit 확장판이라고도 볼 수 있죠. JUnit과는 달리 어노테이션을 사용하지 않고 람다를 사용해서 테스트 코드를 작성한다는 특징이 있습니다.
의존성 설정
Kotest를 사용하려면 다음과 같이 테스트 코드 실행에 JUnit을 사용한다는 설정이 되어있어야 합니다. 일반적으로 그레이들 프로젝트를 생성하면 기본적으로 포함되어 있습니다.
build.gradle
test {
useJUnitPlatform()
}
build.gradle.kts
tasks.test {
useJUnitPlatform()
}
이후 dependencies 블록에 아래 의존성을 추가해야 합니다.
build.gradle
// Kotest 핵심 의존성
testImplementation 'io.kotest:kotest-runner-junit5:${version}'
build.gradle.kts
// Kotest 핵심 의존성
testImplementation("io.kotest:kotest-runner-junit5:${version}")
글을 작성한 2025-07-07 기준으로 kotest-runner-junit5의 정식 최신 버전은 5.9.1입니다.
테스트 스타일
Kotest는 10가지의 다양한 테스트 스타일을 제공하므로, 원하는 스타일에 맞게 테스트 코드를 작성할 수 있습니다. Kotest에서 제공하는 테스트 스타일들의 종류는 다음과 같습니다.
AnnotationSpecFunSpecShouldSpecExpectSpecStringSpecBehaviorSpecDescribeSpecFreeSpecWordSpecFeatureSpecExpectSpec
이 테스트 스타일들은 모두 추상 클래스로 작성되어 있고, 테스트 클래스에서 상속받는 형태로 사용합니다. 테스트 코드는 추상 클래스의 생성자에 람다로 작성해서 구성합니다. 클래스 본문의 init 블록에 작성해서 구성할 수도 있지만 일반적으로 사용되는 방법은 아닙니다.
class KotestTest : StringSpec({
// 이곳에 테스트 코드를 작성합니다.
})
Assertion 함수들
Kotest는 테스트 코드에서 결과를 검증하기 위한 다양한 Assertion 함수들을 제공합니다. 자주 사용되는 Kotest의 값 검증 함수들이 갖는 가장 큰 특징은 infix 함수이기 때문에 테스트 코드를 문장처럼 읽을 수 있다는 점입니다.
예를 들어
// JUnit
Assertions.assertEquals(2, 1 + 1)
JUnit에서는 이렇게 검증하고,
// AssertJ
assertThat(1 + 1).isEqualTo(2)
AssertJ에서는 이렇게 검증하던 것을,
// Kotest
1 + 1 shouldBe 2
Kotest에서는 이렇게 자연어 문장처럼 검증합니다.
자주 사용되는 값 검증 함수로는 shouldBe, shouldNotBe, shouldBeGreaterThan, shouldBeLessThan, shouldContain, shouldHaveSize 등이 있습니다.
만약 예외가 발생하는지 검증하고 싶다면 shouldThrow를 사용해서 제네릭에 예상되는 예외의 타입을 입력하고, 람다 블록 내부에 예외가 발생할 수 있는 코드를 작성하면 됩니다.
class KotestDemo : FunSpec({
test("demo") {
1 + 1 shouldBe 2 // 값이 같은지
"Kotlin" shouldNotBe null // 값이 같지 않은지
10 shouldBeGreaterThan 5 // 값이 더 큰지
5 shouldBeLessThan 10 // 값이 더 작은지
"Hello" shouldContain "H" // 문자열에 특정 문자가 포함되어 있는지
listOf(1, 2, 3) shouldContain 2 // 리스트에 특정 값이 포함되어 있는지
listOf(1, 2, 3) shouldHaveSize 3 // 리스트의 크기가 특정 값인지
shouldThrow<IllegalArgumentException> { // 예외가 발생하는지
throw IllegalArgumentException("This is an error")
}
}
})
대표적인 테스트 스타일 예시
Kotest에서 주로 사용되는 대표적인 테스트 스타일의 예시를 한번 보겠습니다.
AnnotationSpec
기존 JUnit과 같은 방식으로 어노테이션을 사용해서 테스트 코드를 작성할 수 있는 스타일입니다. 다른 테스트 스타일과 달리 생성자에 람다 형태로 테스트 코드를 작성하지 않고, 유일하게 클래스 본문에 테스트 코드를 작성하는 방법이기도 합니다.
다만 이 스타일을 사용하기보다 그냥 JUnit을 사용하는게 낫다보니, AnnotationSpec은 Kotest를 사용하는 의미가 퇴색됩니다. 그래서 JUnit에서 Kotest로 전환하는 과도기에 주로 사용되는 편입니다.
class AnnotationSpecDemo : AnnotationSpec() {
@Test
fun `2 + 3은 5이다`() {
val a = 2
val b = 3
val sum = a + b
sum shouldBe 5
}
@BeforeEach
fun setup() {
println("각 테스트 실행 전 실행")
}
@AfterEach
fun tearDown() {
println("각 테스트 실행 후 실행")
}
}

FunSpec, ShouldSpec, ExpectSpec
함수를 호출하는 형태로 테스트 코드를 작성하는 방법입니다. 각각의 케이스에 대한 테스트 코드는 test 함수의 람다에 작성하고, 공통된 관심사에 대한 여러 개의 테스트 케이스를 묶어서 context 함수의 람다에 작성합니다. context와 test의 인자로 전달되는 문자열은 테스트에 대한 설명이 됩니다.
class FunSpecDemo : FunSpec({
context("덧셈 컨텍스트") {
beforeEach { println("덧셈 컨텍스트의 beforeEach") } // 덧셈 컨텍스트 내의 각 테스트 실행 전 실행
afterEach { println("덧셈 컨텍스트의 afterEach") } // 덧셈 컨텍스트 내의 각 테스트 실행 후 실행
println("덧셈 컨텍스트 시작 전 1번만 실행(지역적 BeforeAll)") // 컨텍스트 시작 전 1번만 실행
test("1 + 1은 2이다") {
val a = 1
val b = 1
val sum = a + b
sum shouldBe 2
}
test("2 + 3은 5이다") {
val a = 2
val b = 3
val sum = a + b
sum shouldBe 5
}
println("덧셈 컨텍스트 끝난 후 1번만 실행(지역적 AfterAll)") // 컨텍스트 끝난 후 1번만 실행
}
context("곱셈 컨텍스트") {
beforeEach { println("곱셈 컨텍스트의 beforeEach") } // 곱셈 컨텍스트 내의 각 테스트 실행 전 실행
afterEach { println("곱셈 컨텍스트의 afterEach") } // 곱셈 컨텍스트 내의 각 테스트 실행 후 실행
test("2 * 3은 6이다") {
val a = 2
val b = 3
val product = a * b
product shouldBe 6
}
test("4 * 5는 20이다") {
val a = 4
val b = 5
val product = a * b
product shouldBe 20
}
}
})

ShouldSpec은 test 대신 should를, ExpectSpec은 test 대신 expect를 사용한다는 차이만 있고 작동 방식은 FunSpec과 대동소이합니다. 표현 방식을 통해 테스트가 어떤 의도를 가졌는지 나타내는 차이만 있을 뿐이기 때문에 원하는 스타일을 사용하면 됩니다.
StringSpec
가장 간단하게 테스트 코드를 작성할 수 있는 스타일입니다. 문자열 리터럴로 테스트 케이스에 대한 설명을 작성하고, 이어지는 람다 블록에 테스트 코드를 작성하면 됩니다.
class StringSpecDemo : StringSpec({
beforeSpec { println("모든 테스트 실행 전 1번만 실행") } // 모든 테스트 실행 전 1번만 실행
afterSpec { println("모든 테스트 실행 후 1번만 실행") } // 모든 테스트 실행 후 1번만 실행
beforeEach { println("각 테스트 실행 전 실행") } // 각 테스트 실행 전 실행
afterEach { println("각 테스트 실행 후 실행") } // 각 테스트 실행 후 실행
"1 + 1은 2이다" {
val a = 1
val b = 1
val sum = a + b
sum shouldBe 2
}
"2 + 3은 5이다" {
val a = 2
val b = 3
val sum = a + b
sum shouldBe 5
}
})

BehaviorSpec
Given-When-Then의 순서로 이뤄지는 BDD 스타일의 테스트 코드를 작성하기에 적합한 스타일입니다. 각 단계를 블록으로 명확하게 나누어, 테스트 시나리오를 단계별로 표현할 수 있습니다.
class BehaviorSpecDemo : BehaviorSpec({
beforeEach { println("Then이 실행되기 전 실행") } // 각 Then 실행 전 실행
afterEach { println("Then이 실행된 후 실행") } // 각 Then 실행 후 실행
Given("2와 3이 있을 때") {
val a = 2
val b = 3
When("덧셈을 수행하면") {
val sum = a + b
Then("결과는 5가 되어야 한다") {
sum shouldBe 5
}
}
When("곱셈을 수행하면") {
val product = a * b
Then("결과는 6이 되어야 한다") {
product shouldBe 6
}
}
}
})

BehaviorSpec을 사용할 때 주의할 점은 beforeEach와 afterEach가 Then 블록 전후에 실행된다는 점입니다. 이로 인해 이전 테스트의 내용을 초기화하기 위해 beforeEach에 DB를 초기화하는 로직이 있다면, When 블록에서 수행한 내용이 초기화되어 정상적인 테스트가 이뤄지지 않을 수 있습니다.
독립된 테스트 케이스 + 테스트 결과 자동 초기화와 같은 방식으로 테스트 코드를 작성하려면 FunSpec이나 StringSpec을 사용하는 것이 적합합니다.
스프링부트 테스트 코드를 작성하는 경우
Kotest로 스프링 테스트 코드를 작성하려면 kotest-runner-junit5 외에 아래 확장 의존성이 추가로 필요합니다.
build.gradle
// Kotest로 스프링 테스트 코드를 작성하기 위한 확장 의존성
testImplementation 'io.kotest.extensions:kotest-extensions-spring:${version}'
build.gradle.kts
// Kotest로 스프링 테스트 코드를 작성하기 위한 확장 의존성
testImplementation("io.kotest.extensions:kotest-extensions-spring:${version}")
글을 작성한 2025-07-07 기준으로 kotest-extensions-spring의 정식 최신 버전은 1.3.0입니다.
위 의존성을 추가하면 JUnit을 사용할 때와 동일하게 @SpringBootTest를 사용해서 테스트 코드를 작성할 수 있습니다. 여기서 JUnit과의 차이점은 스프링 빈을 생성자 주입 방식으로 주입할 때 @Autowired를 명시하지 않아도 된다는 점입니다.
[JUnit5] DB호출 테스트 중 발생한 No ParameterResolver registered for parameter in constructor 에러 해결
@SpringBootTest
class UserServiceTest(
private val userService: UserService,
private val userRepository: UserRepository
) : StringSpec({
"사용자를 추가한다" {
val user = User("홍길동")
userService.register(user)
val savedUser = userRepository.findByIdOrNull(1L)!!
savedUser.name shouldBe "홍길동"
}
})

Kotest의 단점
Kotest가 코틀린에 특화되어 간단한 테스트 코드를 작성할 수 있다는 장점이 있지만 몇가지 단점도 있습니다.
가장 먼저, 테스트 코드를 실행하기 어렵습니다. JUnit은 인텔리제이 같은 IDE 자체적으로 지원을 하지만, Kotest는 그렇지 않습니다. 기본적으로는 아래와 같이 그레이들로 테스트를 실행해야 하죠.
./gradlew test
특정 클래스의 테스트 코드만 실행하고 싶다면 터미널에 아래와 같이 명령어를 입력해야 합니다.
./gradlew test --tests "com.example.demo.StringSpecDemo"
이 문제는 Kotest 플러그인을 설치하면 해결되긴 합니다.

두번째로, JUnit과 달리 테스트 코드의 실행 단위가 클래스라는 점입니다. JUnit는 메소드 단위로 실행할 수 있기 때문에 특정 테스트 메소드만 따로 초기화해서 실행할 수 있지만, Kotest는 해당 테스트 메소드가 포함된 클래스에 작성된 모든 테스트 코드가 실행됩니다.
위의 kotest 플러그인을 사용하면 특정 테스트 메소드를 따로 실행할 수 있긴 합니다. 하지만 근본적으로는 해당 클래스의 모든 테스트 코드를 초기화한 다음, 실행하려고 한 특정 테스트 코드만 실행하고 나머지는 중단하는 방식이기 때문에 특정 테스트 메소드만 실행하기에는 상대적으로 속도가 느려서 효율이 떨어지는 편입니다.

JUnit을 사용하면서 Kotest의 Assertion만 사용하고 싶은 경우
Kotest를 사용하기는 꺼려지지만 Kotest의 Assertion이 가진 간결한 검증 함수를 사용하고 싶을 수도 있습니다. 이 경우에는 아래와 같이 Assertion만 제공하는 의존성을 추가해서, JUnit에서도 Kotest의 Assertion 함수를 사용할 수 있습니다.
build.gradle
// Kotest의 Assertion 함수만 사용하기 위한 의존성
testImplementation 'io.kotest:kotest-assertions-core:${version}'
build.gradle.kts
// Kotest의 Assertion 함수만 사용하기 위한 의존성
testImplementation("io.kotest:kotest-assertions-core:${version}")
kotest-assertions-core 역시 kotest-runner-junit5와 마찬가지로 2025-07-07 기준 정식 최신 버전은 5.9.1입니다.
이렇게 하면 JUnit을 그대로 사용하면서 shouldBe 등의 함수도 함께 사용할 수 있습니다.
class KotestAssertionDemo {
@Test
fun `1 + 1은 2이다`() {
val a = 1
val b = 1
val sum = a + b
sum shouldBe 2
}
@Test
fun `2 + 3은 5이다`() {
val a = 2
val b = 3
val sum = a + b
sum shouldBe 5
}
}

마치며
이상으로 코틀린에 특화된 테스트 프레임워크인 Kotest에 대해 간단하게 정리해봤습니다.
보다 자세한 내용은 Kotest 공식 문서를 참고하실 수 있습니다.
참조 문서
Introduction | Kotest
introgif
kotest.io
GitHub - kotest/kotest: Powerful, elegant and flexible test framework for Kotlin with assertions, property testing and data driv
Powerful, elegant and flexible test framework for Kotlin with assertions, property testing and data driven tests. - kotest/kotest
github.com
'Programming Language > Kotlin & Java' 카테고리의 다른 글
| [Kotlin/Java] JUnit을 사용한 단위 테스트에서 System.in을 사용하는 콘솔 입력 로직 테스트하기 (0) | 2023.12.07 |
|---|---|
| [Java] UnmodifiableList는 진짜 불변 리스트가 아니다 (0) | 2023.08.15 |
| [Java] Java 14부터 추가된 Record 타입과 Kotlin의 Data Class 비교 (0) | 2023.06.14 |
| [Kotlin] 확장 함수와 람다를 사용해서 중복 코드를 제거해보자! (0) | 2023.05.08 |
| [Java/Kotlin] 필드(Field)와 프로퍼티(Property)는 무슨 차이가 있을까? (1) | 2023.05.02 |
댓글