본문 바로가기
CS/Kotlin

kotest 로 Kotlin Test Code 작성하기

by clearinging 2022. 9. 30.
반응형

Kotest

Junit의 단점

  • 한눈에 given when then 구분 어려움
  • 중복 코드 많음 -> 이부분은 하위에 중복 코드 제거 부분에서 언급하겠습니다.
  • 테스트 스타일이 한정적 -> 단위 테스트 특화
  • Junit AssertJ, Mockito를 사용하면 Mocking이나 Assertions 과정에서 kotlin DSL 활용 불가

kotest 장점

  • nested test code의 가독성을 가져올 수 있음
  • DSL(Given( When( Then() ) )) 과같은 구성으로 좀더 명확하게 구분을 지을 수 있음 -> 가독성 증가
  • Kotlin는 멀티 플랫폼이므로 다양한 플랫폼의 스타일이 가능
    • 당양한 test layout 제공
    • ex) 스칼라, 루비 ...etc

kotest 단점

  • 러닝 커브가 존재

의존성 추가 방법

testImplementation("io.kotest:kotest-runner-junit5:5.4.2") // kotlin junit 처럼 쓸 수 있는 Spec 들이 정의 됨
testImplementation("io.kotest:kotest-assertions-core:5.4.2") // shouldBe... etc 와같이 Assertions 의 기능을 제공
testImplementation("io.kotest.extensions:kotest-extensions-spring:1.1.2") // spring boot test 를 위해서 추가

FunSpec

class CalFunSpec : FunSpec({
    test("1과 2를 더하면 3이 반환된다") {
        val stub = Calculator()

        val result = stub.calculate("1 + 2")

        result shouldBe 3
    }

    context("enabled test run") {
        test("test code run") { // 실행
            val stub = Calculator()

            val result = stub.calculate("1 + 2")

            result shouldBe 3
        }

        xtest("test code not run") { // 실행 하지 않음
            val stub = Calculator()

            val result = stub.calculate("1 + 2")

            result shouldBe 3
        }
    }

    xcontext("disabled test run") { // 하위 모두 미 실행
        test("test code run but outer context is disabled") { // 미실행
            val stub = Calculator()

            val result = stub.calculate("1 + 2")

            result shouldBe 3
        }

        xtest("test code not run") { // 미 실행
            val stub = Calculator()

            val result = stub.calculate("1 + 2")

            result shouldBe 3
        }
    }
})
  • test 뒤에 String 값으로 test code에 대한 설명을 추가할 수 있습니다
  • 필드 변수 사용이 불가능하므로 함수 테스트에 주로 사용 됩니다
  • junit에 @Disabled 와 같이 xcontextxtest를 통해서 test code를 실행에서 제외할 수 있습니다.

Describe Spec

class CalDescribeSpec : DescribeSpec({
    val stub = Calculator()

    describe("calculate") {
        context("식이 주어지면") {
            it("해당 식에 대한 결과 값이 반환 된다") {
                calculations.forAll { (expression, data) ->
                    val result = stub.calculate(expression)

                    result shouldBe data
                }
            }
        }
    }
})
  • spring 진영에서는 BDD(given, when, then) 쓰고 있고 Ruby나 JS에서도 이와 비슷하게 describe, it 키워드를 사용해서 test code를 작성할 수 있습니다(DCI(Describe, Context, It) layout 지원)
  • 위 code에서 context는 생략 해도 됩니다.
  • FunSpec과 동일하게 xdescribexit을 사용하면 해당 case는 실행할 필요가 없습니다.

Behavior Spec

class CalBehaviorSpec : BehaviorSpec({
    val stub = Calculator()

    Given("calculator") {
        // before Each 라고 생각 하기
        val expression = "1 + 2"

        When("1과 2를 더한면") {
            val result = stub.calculate(expression)
            Then("3이 반환 된다") {
                result shouldBe 3
            }
        }

        When("1 + 2 결과 와 같은 String 입력시 동일한 결과가 나온다") {
            val result = stub.calculate(expression)
            Then("해당 하는 결과값이 반환된다") {
                result shouldBe stub.calculate("1 + 2")
            }
        }
    }
})
  • BDD 스타일의 test code를 제공합니다
  • 우리가 아는 given, when, then을 제공합니다
  • xgiven, xwhen, xthen 을 통해서 test code disable 할 수 있다

AnnotationSpec

class AnnotationSpecExample : AnnotationSpec() {

    @BeforeEach
    fun beforeTest() {
        println("Before each test")
    }

    @Test
    fun test1() {
        1 shouldBe 1
    }

    @Test
    fun test2() {
        3 shouldBe 3
    }
}
  • 우리가 사용하는 Junit과 가장 비슷한 Spec 입니다
  • kotlin junit test code 를 kotest로 마이그레이션 할 때 사용하면 가장 편리하게 사용할 수 있습니다

Kotest가 Junit 보다 중복 코드가 적은 이유

class CalBehaviorSpec : BehaviorSpec({
    val stub = Calculator()

    Given("calculator") {
        // before Each 라고 생각 하기
        val expression = "1 + 2"

        When("1과 2를 더한면") {
            val result = stub.calculate(expression)
            Then("3이 반환 된다") {
                result shouldBe 3
            }
        }

        When("1 + 2 결과 와 같은 String 입력시 동일한 결과가 나온다") {
            val result = stub.calculate(expression)
            Then("해당 하는 결과값이 반환된다") {
                result shouldBe stub.calculate("1 + 2")
            }
        }
    }
})
class CalBehaviorSpec {
    Calculator stub = Calculator();
    String expression;

    @BeforeEach() {
        this.expression = "1 + 2";
    }

    void calculator_test() {
        // given

        // when
        int result = stub.calculate(expression);

        // then
        assertThat(result).isEqualTo(3);
    }

    void addOneTowResultStringSame_test() {
        // given

        // when
        int result = stub.calculate(expression);

        // then
        assertThat(result).isEqualTo(stub.calculate("1 + 2"));
    }
}
  • 위 java와 kotlin test code는 동일 합니다.
  • Junit인 경우 @BeforeEach를 통해서 모든 test code의 중복로직을 실행 할 수 있습니다.
  • 그리고 만약 특정 test code에서 @BeforeEach가 달라지는 경우 Test Class를 분리 하거나, @Nested class를 정의하고 @BeforeEach 를 추가 정의 해야 하는 한계가 존재합니다(아래 코드 참고)
  • kotest인 경우 Given("") 하위에 작성한 Code는 Given 하위에 존재하는 When Then 모두 사용할 수 있으므로 @BeforeEach와 동일한 결과를 가져다 주고, 위와 같이 간결하게 구현할 수 있습니다
class CalBehaviorSpec : BehaviorSpec({
    val stub = Calculator()

    Given("calculator") {
        // before Each 라고 생각 하기
        val expression = "1 + 2"

        When("1과 2를 더한면") {
            val result = stub.calculate(expression)
            Then("3이 반환 된다") {
                result shouldBe 3
            }
        }

        When("1 + 2 결과 와 같은 String 입력시 동일한 결과가 나온다") {
            val expression1 = "1 + 3"
            val expression2 = "2 + 4"

            val result1 = stub.calculate(expression)
            val result2 = stub.calculate(expression1)
            val result3 = stub.calculate(expression2)

            Then("해당 하는 결과값이 반환된다") {
                result1 shouldBe stub.calculate("1 + 2")
                result2 shouldBe stub.calculate("1 + 3")
                result3 shouldBe stub.calculate("2 + 4")
            }

            Then("상수 값 비교") {
                result1 shouldBe 3
                result2 shouldBe 4
                result3 shouldBe 6
            }
        }
    }
})
class CalBehaviorSpec {
    Calculator stub = Calculator();
    String expression;

    @BeforeEach() {
        this.expression = "1 + 2";
    }

    void calculator_test() {
        // given

        // when
        int result = stub.calculate(expression);

        // then
        assertThat(result).isEqualTo(3);
    }

    @Nested
    class NestedClass {
        String expression1;
        String expression2;

        int result1;
        int result2;
        int result3;

        @BeforeEach() {
            this.expression1 = "1 + 3";
            this.expression2 = "2 + 4";

            this.result1 = stub.calculate(expression);
            this.result2 = stub.calculate(expression1);
            this.result3 = stub.calculate(expression2);
        }

        void addOneTowResultStringSame_test() {
            // given

            // when

            // then
            assertThat(result1).isEqualTo(stub.calculate("1 + 2"));
            assertThat(result2).isEqualTo(stub.calculate("1 + 3"));
            assertThat(result3).isEqualTo(stub.calculate("2 + 4"));
        }

        void constVal_test() {
            // given

            // when

            // then
            assertThat(result1).isEqualTo(stub.calculate(3));
            assertThat(result2).isEqualTo(stub.calculate(4));
            assertThat(result3).isEqualTo(stub.calculate(6));
        }
    }
}
  • 위 java, kotlin code 동일합니다.
  • kotlin은 Nested class 선언 없이 바로 Then 문장을 공통 로직을 작성하면 되었지만, Java는 @Nested class를 생성해서 추가로 @BeforeEach를 정의 해야지 공통 로직을 세분화 할 수 있습니다.
  • 중복을 회피 하기 위해서 @Nested class를 생성하는 방법 밖에 없으므로, 가독성과 간결성이 떨어질 수 밖에 없습니다
  • kotest 는 DSL형태로 When 하위에 Then을 여러개 추가해서 처리할 수 있으므로 간결성을 증대시켜 코드 중복을 제거하고, 가독성 또한 증대 시킬 수 있습니다.

Kotest Assertions

  1. Match
// 기본형
name shouldBe "sam" // assertThat(name).isEqualTo("sam")
name shouldNotBe null // assertThat(name).isNull()

// 체인형 -> 여러 조건을 chaining 할 수 있습니다
myImageFile.shouldHaveExtension(".jpg").shouldStartWith("https").shouldBeLowerCase()
  1. Inspectors(점검자)
  • test code의 collection이 있다면 element에 대한 test를 진행
mylist.forExactly(3) {
    it.city shouldBe "Chicago"
} // my list중 정확히 3개의 element의 city가 Chicargo 이다

val xs = listOf("sam", "gareth", "timothy", "muhammad")

xs.forAtLeast(2) { // 최소 2개의 요소가 lamdba 식이 true여야한다
    it.shouldHaveMinLength(7) // length가 7 보다 이하일 경우 true 초과 일 경우 false
}
  1. Exceptions
  • test 실행 결과 exception 발생
shouldThrow {
    assertThrows { }
    // code in here that you expect to throw an IllegalAccessException
}

Spring Boot 와 kotest

설정

testImplementation("io.kotest.extensions:kotest-extensions-spring:1.1.2") // spring boot test 를 위해서 추가
testImplementation("com.ninja-squad:springmockk:3.1.1") // junit 의 @MockBean @SpyBean 과 같은 기능을 제공 해주는 lib
testImplementation("io.mockk:mockk:1.12.8") // unit test 에서 mockking 사용

Spring Test

@SpringBootTest
internal class CalSpringBootBehavioWithMockSpec : BehaviorSpec() {
    override fun extensions() = listOf(SpringExtension)

    @Autowired
    private lateinit var calculatorService: CalculatorService

    @MockkBean
    private lateinit var mockComponent: MockComponent

    init {
        this.Given("calculate") {
            When("식이 주어지면") {
                Then("해당 식에 대한 결과값이 반환된다") {
                    calculations.forAll { (expression, data) ->
                        val result = calculatorService.calculate(expression)

                        result shouldBe data
                    }
                }
            }
        }

        this.Given("Mocking 한 값과 합을 구한다") {
            every { mockComponent.returnOne() } answers { 2 }

            When("덧셈 로직 실행") {
                val result = calculatorService.calPlus(2)
                Then("덧셈 결과") {
                    result shouldBe 4
                }
            }
        }
    }

    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
        )
    }
}
  • override fun extensions() = listOf(SpringExtension) 를 통해서 spring extension을 추가 해줘야 합니다
  • 위에서 CalculatorServiceMockComponent 를 가지고 있습니다
  • 그리고 MockComponent를 mocking하고 싶을 경우 springmockk@MockkBean 이나 @SpykBean 을 선언해서 Spring Boot의 @MockBean @SpyBean 기능을 사용할 수 있습니다
반응형

'CS > Kotlin' 카테고리의 다른 글

Kotlin DSL  (0) 2022.10.07