Android: Kotest 알아보기 #2

rivermoon·2025년 4월 3일
post-thumbnail

해당 아티클은 모두 공식문서를 기반으로 작성됩니다.

Introduction

지난 글에서 우리는 Kotest를 처음으로 환경에 적용해보고,
Nested Test, Dynamic Test, Lifecycle Callback 같은 기본적인 개념을 배워봤습니다.
이번에는 본격적으로 Kotest가 제공하는 Testing Styles을 알아볼 예정입니다.

Testing Styles

Kotest가 강력한 이유 중 하나는 다양한 테스트 스타일을 제공한다는 점입니다.
프로젝트나 팀 스타일에 맞게 선택하여 사용할 수 있으며, 테스트 목적에 맞게 더 읽기 쉬운 구조를 만들 수 있습니다.

Kotest에서는 총 10가지 테스트 레이아웃을 제공하는데, 하나 하나 살펴보겠습니다.

FunSpec

가장 기본적인 스타일이며. 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 인가요?
    }
})

xcontextxtest 변형을 사용하여 테스트를 비활성화 할 수 있어요.

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
        }
    }
})

이런 식으로 일시적으로 특정 테스트나 블록을 꺼두고 싶을 때 유용하게 사용합니다.

x prefix의 의미가 뭘까..?

Kotest에서 x는 disable의 convention 이에요.
다른 테스트 프레임워크에서도 비슷한 컨벤션을 종종 볼 수 있는데, Kotest에서는 다음과 같은 의미로 사용됩니다!
즉, x가 붙으면 "임시로 꺼둔다", 즉 테스트를 비활성화(disable)한다. 라고 생각하면 되요.

왜 굳이 x를 붙였을까?

skip(), ignore() 같은 함수명을 쓸 수도 있었지만,
Kotest는 테스트 선언부를 읽기 쉬운 DSL 형태로 만들고 싶었어요.

그래서 test나 context 앞에 x만 붙이면,
눈으로 보기만 해도 "아 이건 꺼진 테스트구나"를 직관적으로 알 수 있죠!

실제로 Kotest 공식 문서에도 다음과 같이 언급 된답니다?

A test or context can be prefixed with an x to disable it


String Spec

JUnit의 @Test와 비슷하게 느껴질 수도 있는 스타일이고, 테스트 이름을 그대로 문자열로 작성합니다.

쉽게 이해하자면?

  • 그냥 "문자열" + { 블록 } 구조로 테스트를 쓴다.
  • 다른 스타일(FunSpec, BehaviorSpec 등)처럼 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()에는 여러가지 설정이 들어갈 수 있어요.

  • enabled : 활성/비활성
  • invocations : 몇 번 반복할지
  • timeout : 제한 시간
  • threads : 몇 개의 쓰레드로 병렬 실행할지

이런 것들이 들어갈 수 있답니다!


ShouldSpec

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("...") {} 로 테스트를 정의
  • 마치 "should return the length of the string?" 이라고 말하는 듯한 느낌?

context()로 그룹핑

ShouldSpeccontext()를 써서 테스트를 그룹화할 수 있어요.

class MyTests : ShouldSpec({
    context("String.length") {  // 그룹 이름
        should("return the length of the string") {
            "sammy".length shouldBe 5
            "".length shouldBe 0
        }
    }
})

안쪽에 여러 개의 should()를 쓸 수 있겠죠?

테스트 비활성화 (xshould, xcontext)

FunSpec과 마찬가지로 ShouldSpecx 접두어로 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
        }
    }
})

Describe Spec

DescribeSpecRuby(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() : 개별 테스트 케이스

비활성화 방법 (xdescribe, xit)

다른 스타일과 마찬가지로 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
        }
    }
})

Behavior Spec

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() 키워드를 써서 추가적인 조건도 표현 가능

and() 사용

class BehaviorSpecAndExample : BehaviorSpec({
    given("빗자루가 있을 때") {
        and("마녀도 같이 있다면") {
            `when`("마녀가 빗자루에 올라타면") {
                and("마녀가 히히 하고 웃는다면") {
                    then("마녀는 날 수 있어야 한다") {
                        // test code
                    }
                }
            }
        }
    }
})

비활성화 (disable)

비활성화도 역시 x 접두어로 쉽게 처리 가능해요.

class BehaviorSpecDisableExample: BehaviorSpec({
    xgiven("이 테스트는 비활성화됨") {
        When("부모에 의해 비활성화됨") {
            then("조상에게 비활성화됨") {
                // test code here
            }
        }
    }
    given("이 테스트는 활성화됨") {
        When("이것도 활성화됨") {
            xthen("이 테스트만 비활성화됨") {
                // test code here
            }
        }
    }
})

WordSpec

WordSpec은 should 중심으로 테스트를 작성하는 스타일로,
자연스럽게 문장처럼 읽히는 테스트를 만들 수 있습니다.

class WordSpecExample  : WordSpec({
    "String.length" should  {
        "문자열의 길이를 반환해야 한다" {
            "sammy".length shouldBe 5
            "".length shouldBe 0
        }
    }
})

When()으로 중첩

class WordSpecWhenExample : WordSpec({
    "Hello" When {
        "길이를 요청하면" should {
            "5를 반환해야 한다" {
                "Hello".length shouldBe 5
            }
        }
        "Bob과 붙이면" should {
            "Hello Bob이 되어야 한다" {
                "Hello " + "Bob" shouldBe "Hello Bob"
            }
        }
    }
})

when 주의사항

  • when은 Kotlin 예약어라서 백틱()을 붙이거나 대문자로 사용해야 되요.

이건 한글로 쓰면 더 가독성이 떨어지는거 같네요..


Free Spec

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" 라고 하는데요.
즉, 맨 마지막(실제 테스트)는 하이픈을 붙이면 안 됩니다!


Feature Spec

FeatureSpec은 BDD 스타일 중에서도 특히 Cucumber 사용 경험자에게 익숙한 feature / scenario 키워드로 테스트를 작성하는 스타일 입니다.

  • feature: 기능 설명
  • scenario: 시나리오(테스트 케이스) 설명
class FeatureSpecExample  : FeatureSpec({
    feature("코카콜라 캔") {
        scenario("흔들면 탄산이 터져야 한다") {
            // test here
        }
        scenario("그리고 맛있어야 한다") {
            // test here
        }
    }
})

비활성화 (disable)

다른 스타일처럼 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
        }
    }
})

Expect Spec

ExpectSpec은 구조상 FunSpec이나 ShouldSpec과 비슷하지만,
특징적으로 expect() 키워드를 사용해서 테스트를 정의하는 스타일 입니다.

class ExpectSpecExample : ExpectSpec({
    expect("테스트") {
        // test code here
    }
})

context()를 활용한 그룹화

class ExpectSpecContextExample : ExpectSpec({
    context("덧셈 기능") {
        expect("1 + 2는 3이어야 한다") {
            (1 + 2) shouldBe 3
        }
    }
})

비활성화 (disable)

다른 스타일처럼 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") {
            // 실행 안 됨
        }
    }
})

Annotation Spec

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
    }
}

지원하는 어노테이션 종류

특징?

  • 구조가 JUnit 4 / 5와 거의 같다.
  • 다른 Kotest 스타일과는 다르게 함수마다 어노테이션을 붙임.
  • JUnit → Kotest로 마이그레이션 할 때 최소한의 수정으로 적용 가능함.

특별히 이점을 주는 Spec이 아니므로 마이그레이션 용으로 봐도 될 것 같습니다?


여기까지 해서 공식문서에서 서술한 Testing Styles를 전부 살펴봤어요.
뭔가 확 와닿지 않은 부분이 있는데 아래 표를 보면 아~ 저때 저 Spec을 활용하면 좋겠구나! 라는 생각이 들 수 있을 것 같아요.

Kotest Spec 스타일 비교표 (2025 기준)

Spec 별 비교

Spec설명구조 키워드장점단점추천 상황
FunSpec가장 기본적인 Speccontext, test익숙하고 단순한 구조
초보자에게 추천
중첩 표현이 다소 제한적단순 유닛 테스트, 기본적인 테스트 구조에 적합
StringSpec가장 최소화된 문법의 Spec"문자열" {}간결한 DSL
설명처럼 읽힘
context 중첩 불가
구조화 어려움
단순한 케이스들, 가벼운 테스트, 빠른 작성
ShouldSpecBDD 스타일에 적합context, should자연스러운 문장형 표현
그룹화 쉬움
구조가 한정적 (context - should)BDD 스타일, 유저 스토리 기반 테스트
DescribeSpecJS, Ruby의 describe/it 스타일describe, it익숙한 스타일 (RSpec, Jest)
자유로운 중첩
너무 깊은 중첩은 가독성 저하Behavior 설명 위주의 테스트, 도메인 단위 테스트
BehaviorSpec정통 BDD 스타일context, given, when, then, and상황, 조건, 행동, 결과를 명확히 표현 가능
테스트가 문서처럼 읽힘
구조가 강제적이라 적응이 필요복잡한 시나리오 테스트
상태 기반 테스트 (Stateful Test)
WordSpec자연어처럼 읽히는 스타일"문장" should {}, When {}문장형 테스트 작성 가능
설명이 읽기 쉬움
구조 표현에 제한 (should - When - should)비즈니스 로직 설명, 유저 스토리 테스트
FreeSpec자유도 높은 구조"문장" - {}, "문장" {}제한 없는 자유로운 중첩 가능너무 자유로워서 구조 남용 주의복잡한 트리형 시나리오, 구조 커스터마이징이 필요한 경우
FeatureSpecCucumber 스타일feature, scenarioFeature / Scenario 구조로 테스트 작성
문서화 용이
구조가 단순해 세밀한 표현 어려움Acceptance Test, 서비스 시나리오 검증
ExpectSpecexpect()을 사용하는 기본 Speccontext, expect결과를 기대하는 느낌의 네이밍
FunSpec과 유사
단순 expect만 있어서 표현력은 FunSpec과 유사간단한 단위 테스트, expect 스타일이 익숙할 때
AnnotationSpecJUnit 스타일 어노테이션 사용@Test, @BeforeEachJUnit에서 손쉽게 마이그레이션 가능Kotest DSL의 장점 활용 어려움JUnit → Kotest 전환 시 최소 수정으로 사용

공통 기능

기능공통 지원 여부설명
비활성화Ox 접두어 지원 (xtest, xcontext, xshould, xit, xfeature 등)
config()O대부분의 Spec에서 개별 테스트에 설정 가능 (enabled, invocations, timeout 등)
중첩일부 제한StringSpec은 context 불가, 나머지는 대부분 중첩 지원
BDD 친화도BehaviorSpec, ShouldSpec, DescribeSpecBDD 테스트에 적합한 키워드 제공
JUnit 유사성AnnotationSpecJUnit 마이그레이션에 최적화

추천 상황

상황추천 Spec
가장 기본적인 유닛 테스트FunSpec
최소한의 코드로 작성StringSpec
읽기 쉬운 BDD 테스트ShouldSpec
RSpec, Jest 느낌DescribeSpec
시나리오형 BDDBehaviorSpec
문장처럼 쓰고 싶을 때WordSpec
자유로운 계층FreeSpec
기능 / 시나리오 중심FeatureSpec
기존 JUnit 코드 이식AnnotationSpec
기대 기반 단위 테스트ExpectSpec

참고

https://kotest.io/docs/framework/testing-styles.html#expect-spec

profile
Android Developer

0개의 댓글