
해당 아티클은 모두 공식문서를 기반으로 작성됩니다.
지난 글에서 우리는 Kotest를 처음으로 환경에 적용해보고,
Nested Test, Dynamic Test, Lifecycle Callback 같은 기본적인 개념을 배워봤습니다.
이번에는 본격적으로 Kotest가 제공하는 Testing Styles을 알아볼 예정입니다.
Kotest가 강력한 이유 중 하나는 다양한 테스트 스타일을 제공한다는 점입니다.
프로젝트나 팀 스타일에 맞게 선택하여 사용할 수 있으며, 테스트 목적에 맞게 더 읽기 쉬운 구조를 만들 수 있습니다.
Kotest에서는 총 10가지 테스트 레이아웃을 제공하는데, 하나 하나 살펴보겠습니다.
가장 기본적인 스타일이며. test("설명") {} 로 테스트를 정의할 수 있습니다.
class FunSpec : FunSpec({
test("String length should return the length of the string") {
"sammy".length shouldBe 5 // 실제로 "sammy"의 length가 5 인가요?
"".length shouldBe 0 // 실제로 ""의 length가 0 인가요?
}
})
xcontext 및 xtest 변형을 사용하여 테스트를 비활성화 할 수 있어요.
xtest: 특정 테스트 메서드만 비활성화할 때 사용
xcontext: 특정 context 블록 전체를 비활성화할 때 사용
class FunSpecDisable : FunSpec({
context("this outer block is enabled") { // context는 활성화
xtest("this test is disabled") { // test는 비활성화
// test here
}
}
xcontext("this block is disabled") { // context 전체가 비활성화
test("disabled by inheritance from the parent") { // 따라서 이 test도 실행 안 됨
// test here
}
}
})
이런 식으로 일시적으로 특정 테스트나 블록을 꺼두고 싶을 때 유용하게 사용합니다.
Kotest에서 x는 disable의 convention 이에요.
다른 테스트 프레임워크에서도 비슷한 컨벤션을 종종 볼 수 있는데, Kotest에서는 다음과 같은 의미로 사용됩니다!
즉, x가 붙으면 "임시로 꺼둔다", 즉 테스트를 비활성화(disable)한다. 라고 생각하면 되요.
skip(), ignore() 같은 함수명을 쓸 수도 있었지만,
Kotest는 테스트 선언부를 읽기 쉬운 DSL 형태로 만들고 싶었어요.
그래서 test나 context 앞에 x만 붙이면,
눈으로 보기만 해도 "아 이건 꺼진 테스트구나"를 직관적으로 알 수 있죠!
실제로 Kotest 공식 문서에도 다음과 같이 언급 된답니다?
A test or context can be prefixed with an
xto disable it
JUnit의 @Test와 비슷하게 느껴질 수도 있는 스타일이고, 테스트 이름을 그대로 문자열로 작성합니다.
쉽게 이해하자면?
context, test 같은 키워드도 필요 없음.class StringSpecExample : StringSpec({
"strings.length should return size of string" {
"hello".length shouldBe 5 // 실제로 "hello"의 length가 5 인가요?
}
})
테스트 하나하나에 설정을 붙이고 싶을 때는 .config()를 써요.
class StringSpecConfigExample : StringSpec({
"string.length should return size of string".config(enabled = false, invocations = 3) {
"hello".length shouldBe 5
}
})
이 예시코드는 해당 설정이 들어간 코드죠.
enabled = false : 이 테스트는 실행 안 함 (비활성화)invocations = 3 : 이 테스트를 3번 반복 실행 (만약 enabled=true 라면?)config()에는 여러가지 설정이 들어갈 수 있어요.
이런 것들이 들어갈 수 있답니다!
FunSpec이랑 비슷한데, test() 대신 should()를 써서 말처럼 읽히게 만드는 스타일 입니다.
class ShouldSpecExample : ShouldSpec({
should("return the length of the string") {
"sammy".length shouldBe 5 // 실제로 "sammy"의 length가 5 인가요?
"".length shouldBe 0 // 실제로 ""의 length가 0 인가요?
}
})
should("...") {} 로 테스트를 정의ShouldSpec도 context()를 써서 테스트를 그룹화할 수 있어요.
class MyTests : ShouldSpec({
context("String.length") { // 그룹 이름
should("return the length of the string") {
"sammy".length shouldBe 5
"".length shouldBe 0
}
}
})
안쪽에 여러 개의 should()를 쓸 수 있겠죠?
FunSpec과 마찬가지로 ShouldSpec도 x 접두어로 disable이 가능해요.
class ShouldSpecDisableExample : ShouldSpec({
context("this outer block is enabled") {
xshould("this test is disabled") { // 실행 안 됨
// test here
}
}
xcontext("this block is disabled") { // 전체 block 비활성화
should("disabled by inheritance from the parent") { // 실행 안 됨
// test here
}
}
})
DescribeSpec은 Ruby(RSpec)이나 JavaScript(Jest, Mocha) 같은 환경에서 자주 쓰는 describe / it 스타일을 따라 만든 것 입니다.
class DescribeSpecExample : DescribeSpec({
describe("score") { // 그룹 이름
it("start as zero") { // 실제 테스트
}
describe("with a strike") { // 중첩 describe
it("adds ten") {
// test here
}
it("carries strike to the next frame") {
// test here
}
}
describe("for the opposite team") {
it("Should negate one score") {
// test here
}
}
}
})
describe() : 테스트 그룹it() : 개별 테스트 케이스다른 스타일과 마찬가지로 x를 붙여서 쉽게 disable이 가능해요.
class DescribeSpecDisableExample : DescribeSpec({
describe("this outer block is enabled") {
xit("this test is disabled") { // 실행 안 됨
// test here
}
}
xdescribe("this block is disabled") { // 아래 it들도 전부 실행 안 됨
it("disabled by inheritance from the parent") {
// test here
}
}
})
BehaviorSpec은 정통 BDD(Behavior Driven Development) 스타일입니다.
context, given, when, then 키워드를 이용해서
테스트를 상황 → 조건 → 행동 → 기대 결과 흐름으로 작성할 수 있어요.
class BehaviorSpecExample : BehaviorSpec ({
context("빗자루는 스스로 날아서 다시 돌아올 수 있어야 한다.") {
given("빗자루가 있을 때") {
`when`("내가 빗자루에 올라타면") {
then("나는 날 수 있어야 한다.") {
// test code here
}
}
`when`("내가 빗자루를 던지면") {
then("빗자루는 날아서 다시 돌아와야 한다.") {
// test code here
}
}
}
}
})
when은 Kotlin 예약어라서 백틱으로 묶어줘야 함When(), Given()처럼 대문자 함수도 지원 (백틱 안 써도 됨)and() 키워드를 써서 추가적인 조건도 표현 가능class BehaviorSpecAndExample : BehaviorSpec({
given("빗자루가 있을 때") {
and("마녀도 같이 있다면") {
`when`("마녀가 빗자루에 올라타면") {
and("마녀가 히히 하고 웃는다면") {
then("마녀는 날 수 있어야 한다") {
// test code
}
}
}
}
}
})
비활성화도 역시 x 접두어로 쉽게 처리 가능해요.
class BehaviorSpecDisableExample: BehaviorSpec({
xgiven("이 테스트는 비활성화됨") {
When("부모에 의해 비활성화됨") {
then("조상에게 비활성화됨") {
// test code here
}
}
}
given("이 테스트는 활성화됨") {
When("이것도 활성화됨") {
xthen("이 테스트만 비활성화됨") {
// test code here
}
}
}
})
WordSpec은 should 중심으로 테스트를 작성하는 스타일로,
자연스럽게 문장처럼 읽히는 테스트를 만들 수 있습니다.
class WordSpecExample : WordSpec({
"String.length" should {
"문자열의 길이를 반환해야 한다" {
"sammy".length shouldBe 5
"".length shouldBe 0
}
}
})
class WordSpecWhenExample : WordSpec({
"Hello" When {
"길이를 요청하면" should {
"5를 반환해야 한다" {
"Hello".length shouldBe 5
}
}
"Bob과 붙이면" should {
"Hello Bob이 되어야 한다" {
"Hello " + "Bob" shouldBe "Hello Bob"
}
}
}
})
when은 Kotlin 예약어라서 백틱()을 붙이거나 대문자로 사용해야 되요.이건 한글로 쓰면 더 가독성이 떨어지는거 같네요..
FreeSpec은 말 그대로 Free한 깊이의 중첩을 지원하는 스타일입니다.
다른 Spec처럼 context, describe, should 같은 키워드가 강제되지 않고,
그냥 문자열과 -(하이픈) 기호만으로 계층 구조를 만들 수 있어요.
class FreeSpecExample : FreeSpec({
"String.length" - {
"문자열의 길이를 반환해야 한다" {
"sammy".length shouldBe 5
"".length shouldBe 0
}
}
"컨테이너는 원하는 만큼 깊게 중첩할 수 있다" - {
"그래서 또 다른 컨테이너를 중첩하고.." - {
"그리고 또 다른 컨테이너.." - {
"실제 테스트!!" {
1 + 1 shouldBe 2
}
}
}
}
})
공식문서에서도 "The innermost test must not use the - (minus) keyword" 라고 하는데요.
즉, 맨 마지막(실제 테스트)는 하이픈을 붙이면 안 됩니다!
FeatureSpec은 BDD 스타일 중에서도 특히 Cucumber 사용 경험자에게 익숙한 feature / scenario 키워드로 테스트를 작성하는 스타일 입니다.
class FeatureSpecExample : FeatureSpec({
feature("코카콜라 캔") {
scenario("흔들면 탄산이 터져야 한다") {
// test here
}
scenario("그리고 맛있어야 한다") {
// test here
}
}
})
다른 스타일처럼 x 접두어로 쉽게 disable이 가능해요.
class FeatureSpecDisableExample : FeatureSpec({
feature("this outer block is enabled") {
xscenario("this test is disabled") { // 비활성화
// test here
}
}
xfeature("this block is disabled") { // feature 자체가 비활성화
scenario("disabled by inheritance from the parent") { // 실행 안 됨
// test here
}
}
})
ExpectSpec은 구조상 FunSpec이나 ShouldSpec과 비슷하지만,
특징적으로 expect() 키워드를 사용해서 테스트를 정의하는 스타일 입니다.
class ExpectSpecExample : ExpectSpec({
expect("테스트") {
// test code here
}
})
class ExpectSpecContextExample : ExpectSpec({
context("덧셈 기능") {
expect("1 + 2는 3이어야 한다") {
(1 + 2) shouldBe 3
}
}
})
다른 스타일처럼 x 접두어로 쉽게 disable이 가능해요.
class ExpectSpecDisableExample : ExpectSpec({
context("this outer block is enabled") {
xexpect("this test is disabled") {
// 비활성화됨
}
}
xcontext("this block is disabled") {
expect("disabled by inheritance from the parent") {
// 실행 안 됨
}
}
})
AnnotationSpec은 이름 그대로 JUnit 스타일의 어노테이션(@Test, @BeforeEach 등)을 사용하는 Kotest의 테스트 스타일입니다.
기존에 JUnit 4/5를 쓰던 사람들에게 익숙한 방식이죠?
class AnnotationSpecExample : AnnotationSpec() {
@BeforeEach
fun beforeTest() {
println("테스트 전 실행")
}
@Test
fun test1() {
1 shouldBe 1
}
@Test
fun test2() {
3 shouldBe 3
}
}

특별히 이점을 주는 Spec이 아니므로 마이그레이션 용으로 봐도 될 것 같습니다?
여기까지 해서 공식문서에서 서술한 Testing Styles를 전부 살펴봤어요.
뭔가 확 와닿지 않은 부분이 있는데 아래 표를 보면 아~ 저때 저 Spec을 활용하면 좋겠구나! 라는 생각이 들 수 있을 것 같아요.
| Spec | 설명 | 구조 키워드 | 장점 | 단점 | 추천 상황 |
|---|---|---|---|---|---|
| FunSpec | 가장 기본적인 Spec | context, test | 익숙하고 단순한 구조 초보자에게 추천 | 중첩 표현이 다소 제한적 | 단순 유닛 테스트, 기본적인 테스트 구조에 적합 |
| StringSpec | 가장 최소화된 문법의 Spec | "문자열" {} | 간결한 DSL 설명처럼 읽힘 | context 중첩 불가 구조화 어려움 | 단순한 케이스들, 가벼운 테스트, 빠른 작성 |
| ShouldSpec | BDD 스타일에 적합 | context, should | 자연스러운 문장형 표현 그룹화 쉬움 | 구조가 한정적 (context - should) | BDD 스타일, 유저 스토리 기반 테스트 |
| DescribeSpec | JS, Ruby의 describe/it 스타일 | describe, it | 익숙한 스타일 (RSpec, Jest) 자유로운 중첩 | 너무 깊은 중첩은 가독성 저하 | Behavior 설명 위주의 테스트, 도메인 단위 테스트 |
| BehaviorSpec | 정통 BDD 스타일 | context, given, when, then, and | 상황, 조건, 행동, 결과를 명확히 표현 가능 테스트가 문서처럼 읽힘 | 구조가 강제적이라 적응이 필요 | 복잡한 시나리오 테스트 상태 기반 테스트 (Stateful Test) |
| WordSpec | 자연어처럼 읽히는 스타일 | "문장" should {}, When {} | 문장형 테스트 작성 가능 설명이 읽기 쉬움 | 구조 표현에 제한 (should - When - should) | 비즈니스 로직 설명, 유저 스토리 테스트 |
| FreeSpec | 자유도 높은 구조 | "문장" - {}, "문장" {} | 제한 없는 자유로운 중첩 가능 | 너무 자유로워서 구조 남용 주의 | 복잡한 트리형 시나리오, 구조 커스터마이징이 필요한 경우 |
| FeatureSpec | Cucumber 스타일 | feature, scenario | Feature / Scenario 구조로 테스트 작성 문서화 용이 | 구조가 단순해 세밀한 표현 어려움 | Acceptance Test, 서비스 시나리오 검증 |
| ExpectSpec | expect()을 사용하는 기본 Spec | context, expect | 결과를 기대하는 느낌의 네이밍 FunSpec과 유사 | 단순 expect만 있어서 표현력은 FunSpec과 유사 | 간단한 단위 테스트, expect 스타일이 익숙할 때 |
| AnnotationSpec | JUnit 스타일 어노테이션 사용 | @Test, @BeforeEach | JUnit에서 손쉽게 마이그레이션 가능 | Kotest DSL의 장점 활용 어려움 | JUnit → Kotest 전환 시 최소 수정으로 사용 |
| 기능 | 공통 지원 여부 | 설명 |
|---|---|---|
| 비활성화 | O | x 접두어 지원 (xtest, xcontext, xshould, xit, xfeature 등) |
| config() | O | 대부분의 Spec에서 개별 테스트에 설정 가능 (enabled, invocations, timeout 등) |
| 중첩 | 일부 제한 | StringSpec은 context 불가, 나머지는 대부분 중첩 지원 |
| BDD 친화도 | BehaviorSpec, ShouldSpec, DescribeSpec | BDD 테스트에 적합한 키워드 제공 |
| JUnit 유사성 | AnnotationSpec | JUnit 마이그레이션에 최적화 |
| 상황 | 추천 Spec |
|---|---|
| 가장 기본적인 유닛 테스트 | FunSpec |
| 최소한의 코드로 작성 | StringSpec |
| 읽기 쉬운 BDD 테스트 | ShouldSpec |
| RSpec, Jest 느낌 | DescribeSpec |
| 시나리오형 BDD | BehaviorSpec |
| 문장처럼 쓰고 싶을 때 | WordSpec |
| 자유로운 계층 | FreeSpec |
| 기능 / 시나리오 중심 | FeatureSpec |
| 기존 JUnit 코드 이식 | AnnotationSpec |
| 기대 기반 단위 테스트 | ExpectSpec |
https://kotest.io/docs/framework/testing-styles.html#expect-spec