Kotest를 사용하여 보기 좋은 테스트 코드 작성하기

Glen·2023년 12월 13일
3

배운것

목록 보기
30/37

서론

어떤 언어로 개발하든, 테스트 코드는 작성할 수 있다.

테스트 코드가 필요한 이유는 작성한 코드가 제대로 작동하는지 검증하는 이유도 있지만, 코드로 표현되는 문서화와 요구 사항의 증명이라고 생각한다.

그만큼 테스트 코드는 중요하고 가독성 있게 잘 작성할 수 있어야 한다.

테스트 코드 작성을 위해 언어마다 제공되는 대표적인 테스트 프레임워크가 있다.

해당 테스트 프레임워크를 사용하면 쉽고 빠르게 테스트 코드 작성이 가능하다.

Java를 사용하면 주로 JUnit을 사용한다.

코틀린 또한 JVM 위에서 동작하고, Java로 작성된 코드 또한 호환이 가능하기 때문에 JUnit을 사용할 수 있다.

하지만 굳이 코틀린을 사용하면서 JUnit만 사용할 필요는 없다.

코틀린과 Java를 비교했을 때 코틀린이 가지는 장점은 간결한 문법과 확장 함수와 중위 함수를 사용할 수 있다는 점이다.

따라서 Java로 작성된 Junit을 사용하면 코틀린이 가지는 장점을 누릴 수 없다.

이러한 점으로 코틀린에서는 Kotest라는 테스트 프레임워크를 제공한다.

Kotest는 코틀린이 가지고 있는 확장 함수와 중위 함수를 토대로 DSL을 제공하기에 가독성이 높고 효과적인 테스트 코드를 작성할 수 있다.

그러면 Kotest에 대해 자세히 알아보자

본론

Kotest

Kotest는 코틀린으로 작성된 테스트 프레임워크이다.

Kotest의 특징 중 하나는 여러 개의 독립형 하위 프로젝트로 나뉘어 있다.

  • Test framework
  • Assertions library
  • Property testing

Kotest는 JVM 위에서 동작할 때 JUnit 플랫폼을 사용한다.

따라서 JUnit 프레임워크가 설치되어 있어야 한다.

스프링 부트를 사용한다면 JUnit이 기본으로 설치되어 있기 때문에 Kotest 의존성만 추가하면 된다.

val kotestVersion = "5.7.2"
dependencies {
    // Test framework
    testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion")
    // Assertion library
    testImplementation("io.kotest:kotest-assertions-core:$kotestVersion")  
}

Assertion library가 나뉘어져 있기에, Kotest를 사용하면서 JUnit5 또는 AssertJ의 Assertion을 사용할 수 있다.

역으로, JUnit에서 Kotest의 Assertion 또한 사용할 수 있다.

class JUnit5Test {  
    @Test  
    fun `1 + 23이다`() {  
        1 + 2 shouldBe 3
    }  
}

이러한 유연함은 테스트 코드 작성 시 선택의 폭을 넓혀준다.

Testing Style

JUnit을 사용했을 때는 단순히 다음과 같이 테스트 코드를 작성할 수 있었다.

class JUnit5Test {

    @Test
    void myTest() {
        ...
    }

    @Nested  
    class outer {
      
        @Test  
        void inner() {  
            ...
        }  
    }
}

JUnit의 방식도 나쁘지 않지만, BDD 기반의 테스트 코드를 작성하기에는 부족한 면이 있다.

따라서 Given-When-Then 주석을 달아 테스트의 행위를 설명하거나, @Nested와 내부 클래스를 활용하여 행위를 설명한다.

class JUnit5Test {

    @Test
    @DisplayName("1과 2를 더하면 3이다.")
    void one_plus_two_is_three() {
        // given
        int first = 1
        int second = 2

        // when
        int actual = first + second

        // then
        assertThat(actual).isEqualTo(3)
    }

    @Nested  
    @DisplayName("1과 2를 더하면")  
    class outer {  
        val actual = 1 + 2  
      
        @Test  
        @DisplayName("3이다.")  
        fun inner() {  
            assertThat(actual).isEqualTo(3)  
        }  
    }
}

이러한 JUnit의 한정적인 기능 때문에 @DisplayName을 사용해야 하고, 클래스 명, 메소드 명을 중복적으로 작성해야 하는 문제점이 생긴다.

게다가 클래스, 메서드 명을 지을 때는 제약이 있으므로 불편함 또한 발생한다.

이러한 점 때문에 JUnit을 사용하면 BDD 기반의 테스트 코드를 작성하는 것이 힘들다.

하지만 Kotest를 사용하면 이러한 JUnit의 문제점을 해결할 수 있다.

// 여러 스타일 중 BehaviorSpec을 사용했다.
class KotestTest : BehaviorSpec({  

    // given("1과 2가 있고")
    Given("1과 2가 있고") {  
        val one = 1  
        val two = 2  

        // `when`("1과 2를 더하면")
        When("1과 2를 더하면") {  
            val actual = one + two  
            
            // then("3이다")
            Then("3이다.") {  
                actual shouldBe 3  
            }  
        }    
    }
})

메서드 또는 클래스 명을 생각하느라 시간을 쓸 필요가 없고, @Nested, @Test 어노테이션 또한 필요 없다.

그저 Kotest Spec 클래스를 상속하고, 상속한 클래스의 매개변수에 람다를 정의하고, 람다에 테스트 코드를 작성하면 된다.

이처럼 Kotest를 사용하면 가독성이 좋고 간결하게 테스트 코드를 작성할 수 있다.

Kotest는 여러 가지의 테스트 스타일을 제공한다.

테스트 스타일영감 받은 테스트
Fun SpecScalaTest
Describe SpecJavascript frameworks and RSpec
Should SpecA Kotest original
String SpecA Kotest original
Behavior SpecBDD frameworks
Free SpecScalaTest
Word SpecScalaTest
Feature SpecCucumber
Expect SpecA Kotest original
Annotation SpecJUnit

위에서 사용한 스타일은 Behavior Spec이다.

몇 가지 스타일을 간단히 알아보자.

Fun Spec

FunSpec은 ScalaTest에서 영감을 받은 스타일이다.

다음과 같이 사용할 수 있다.

class KotestTest : FunSpec({

    test("1과 2를 더하면 3이다.") {
        val actual = 1 + 2
        
        actual shouldBe 3
    }
})

String Spec

StringSpec을 사용하면 구문을 최소화 할 수 있다.

class KotestTest : FunSpec({

    "1과 2를 더하면 3이다." {
        val actual = 1 + 2
        actual shouldBe 3
    }
})

Behavior Spec

BehaviorSpec을 사용하면 BDD 스타일의 테스트 코드를 쉽게 작성할 수 있다.

// 여러 스타일 중 BehaviorSpec을 사용했다.
class KotestTest : BehaviorSpec({  

    // given("1과 2가 있고")
    Given("1과 2가 있고") {  
        val one = 1  
        val two = 2  

        // `when`("1과 2를 더하면")
        When("1과 2를 더하면") {  
            val actual = one + two  
            
            // then("3이다.")
            Then("3이다.") {  
                actual shouldBe 3  
            }
        }    
    }
})

when은 코틀린의 예약어이기 때문에 백틱을 사용하여 `when()` 과 같이 사용하거나, Given(), When(), Then()을 메서드를 사용하면 된다.

Describe Spec

DescribeSpec을 사용하면 describe(), it() 메서드를 통해 테스트 코드를 작성할 수 있다.
또한, DCI 패턴을 사용하여 BDD 스타일과 차별점을 둔 테스트를 작성할 수 있다.

class KotestTest : DescribeSpec({

    describe("plus") {
        context("1과 2를 더하면") {
            val actual = 1 + 2
            
            it("3이다.") {
                actual shouldBe 3
            }
        }
    }
})

Annotation Spec

AnnotationSpec을 사용하면 JUnit 테스트 코드 스타일로 테스트를 작성할 수 있다.

class KotestTest : AnnotationSpec({

    @Test
    fun `12를 더하면 3이다`() {
        val actual = 1 + 2
        
        actual shouldBe 3
    }
})

AnnotationSpec은 JUnit을 사용하는 것보다 많은 이점을 제공하지 않지만, 기존 JUnit으로 작성된 테스트를 Kotest로 마이그레이션 할 때 변경 없이 가져올 수 있다.

결론

테스트 코드는 단순 검증을 넘어, 사람이 읽었을 때 가독성이 좋고 쉽게 요구 사항의 증명을 나타낼 수 있어야 한다.

이는 코틀린과 Kotest의 조합으로 쉽게 표현할 수 있다.

Kotest가 가지는 여러 스타일의 테스트 코드로 효과적인 테스트 코드를 작성하여 개발의 편의성과 가독성이 높은 테스트 코드를 작성할 수 있다.

또한 공식 문서에서 나와 있듯 한 가지 스타일만 고집하여 사용하지 않고, 상황에 맞게 효과적인 스타일을 사용해야 한다.

참고할 점은 Kotest를 Spring에 종속적인 테스트를 수행할 때 Spring에 대한 지원이 기본으로 제공되지 않기 때문에 별도 라이브러리를 추가해야 하며, 추가로 설정해야 하는 부분이 있으므로 주의해야 한다.

참조

profile
꾸준히 성장하고 싶은 사람

1개의 댓글

comment-user-thumbnail
2024년 6월 19일

코테스트 정리 감사합니다

답글 달기