본문 바로가기
개발/Java, Kotlin

[Kotlin test framework] Kotest

by 달사쿠 2021. 8. 21.
반응형

Document

 

Kotest | Kotest

Flexible, powerful and elegant kotlin test framework with multiplatform support

kotest.io

 


Introduce

  • Kotest는 Kotlin을 위한 테스팅 도구
  • Kotlin-Test에서 Kotest로 이름이 변경되었으며, 이름에서 알 수 있듯이 Kotlin언어로 작성된 코드를 테스트하기에 아주 용이
  • Spock와 유사하게 간결한 코드와 다양한 유형의 테스트 방식을 지원
  • Kotest는 multi-platform test framework로, 각 하위 프로젝트는 독립적으로 사용할 수 있습니다.
    • Test Framework
    • Assertion Library
    • Property Testing
  • 문서화가 잘되어있긴 하지만, 아직 도큐먼트나 참고할 레퍼런스들이 아직 다양하지 않은 것 같다. (2021년 8월 기준)

 


도입해보게된 배경

  • 팀에서 주로 사용하는 Spock framework (Test with Spock framework) 와 Kotlin + Spring 조합 사용 시, Spring Component Annotation을 붙이지 않은 클래스의 mock 객체가 생성 되지 않는 문제가 발생 (실제 내부로직을 타고 들어감)
    • 추측되는 원인
      • 기본적으로 Kotlin 클래스의 경우, class에 별다른 키워드를 붙이지 않는 경우 final 클래스로 클래스가 생성
      • Groovy의 경우, Mockito와 비슷한 형태를 띄고 있고 Mockito의 Mock 또한 final class를 mocking 할 수 없는 문제를 갖고 있어 발생한 문제로 보여짐
      • class를 open class로 변경해서 mock 객체를 만드니 성공
  • Kotlin 친화적인 테스팅 도구 서칭

 


How to use

Quick Start : 의존성 설정

JVM/Gradle

  • 사용할 기능들에 해당하는 라이브러리만 가져다 사용하면 됩니다.
test {
    maxHeapSize = "1024m"
    useJUnitPlatform()
}
...
dependencies {
    testImplementation("io.kotest:kotest-runner-junit5-jvm:$kotestVersion") //Test Framework
    testImplementation("io.kotest:kotest-assertions-core:$kotestVersion")   //Assertions Library
    testImplementation("io.kotest:kotest-property:$kotestVersion")          //Property Testing
    testImplementation("io.kotest:kotest-extensions-spring:$kotestVersion") //Spring Extensions
}

 

IntelliJ Plugin

Kotest는 Jetbrains 플러그인 마켓플레이스(IntelliJ 내에서 검색)에서 사용할 수 있는 IntelliJ 플러그인을 제공합니다.

이 플러그인은 각 테스트에 대한 실행 아이콘, 테스트 탐색을 위한 도구 창, 중복 테스트 강조 표시, assertion intentions 등을 제공합니다.

Intellij 플러그인을 사용하기 위해서는 Kotest 4.2 이상이 필요합니다.

 

Testing Style (Spec)

  • Kotest는 10가지 스타일의 테스트 레이아웃을 제공합니다. (일부는 다른 인기 있는 테스트 프레임워크에서 영감을 받아 제작)
  • Kotest를 사용하려면 테스트 스타일 중 하나를 Extend하는 클래스 파일을 만들어 사용
  • 그런 다음 init { } 블록 내에서 테스트 케이스를 만듬
  • 스타일 간에 기능적 차이는 없음
  • 모두 동일한 유형의 구성(스레드, 태그 등)을 허용
  • 테스트를 구성하는 방법은 단순히 선호도의 문제

 

Example

String Spec (A Kotest original)

class MyTests : StringSpec({
    "strings.length should return size of string" {
        "hello".length shouldBe 5
    }
})

 

Fun Spec (ScalaTest)

class MyTests : FunSpec({
    test("String length should return the length of the string") {
        "sammy".length shouldBe 5
        "".length shouldBe 0
    }
})

 

Behavior Spec (BDD Framework)

class MyTests : BehaviorSpec({
    given("a broomstick") {
        //given condition
        `when`("I sit on it") {
            //when condition
            then("I should be able to fly") {
                // test code
            }
        }
        `when`("I throw it away") {
            then("it should come back") {
                // test code
            }
        }
    }
})

 

Feature Spec (Cucumber)

class MyTests : FeatureSpec({
    feature("the can of coke") {
        scenario("should be fizzy when I shake it") {
            // test here
        }
        scenario("and should be tasty") {
            // test here
        }
    }
})

 

Annotation Spec (JUnit)

class AnnotationSpecExample : AnnotationSpec() {
 
    @BeforeEach
    fun beforeTest() {
        println("Before each test")
    }
 
    @Test
    fun test1() {
        1 shouldBe 1
    }
 
    @Test
    fun test2() {
        3 shouldBe 3
    }
}

 


Test Listeners

  • 테스트 전/후, Spec 클래스의 모든 테스트 전/후, 전체 프로젝트 전/후 등과 같은 테스트 엔진 생명 주기에 어떤 세팅을 할 때 사용할 수 있도록 TestListener 인터페이스 제공
Function 목적
beforeTest 테스트 케이스가 실행되기 전에 매번 호출됩니다. 무시되는 테스트(아래의 Test Ignore 참고) 에서는 실행되지 않습니다.
afterTest 테스트 케이스가 실행된 후 매번 호출됩니다. 무시되는 테스트에서는 실행되지 않습니다. 테스트가 실패하더라도 실행됩니다.
beforeSpec 모든 beforeTest 함수가 호출되기 전에, Spec이 시작될 때마다 호출됩니다.
afterSpec 모든 afterTest 함수가 호출된 후 Spec이 완료될 때마다 호출됩니다.
beforeSpecClass 엔진이 실행할 Spec을 준비할 때 호출됩니다. Spec이 인스턴스화되는 횟수에 관계없이 한 번만 실행됩니다.
afterSpecClass 사양이 인스턴스화되는 횟수에 관계없이 사양에 대한 모든 테스트가 완료되면 호출됩니다.
beforeProject 테스트 엔진이 시작되는 즉시 호출됩니다.
afterProject 테스트 엔진이 완료되는 즉시 호출됩니다.
afterDiscovery 모든 Spec 클래스를 검색하고, beforeSpec 함수가 호출되기 전과 테스트 엔진에서 Spec을 인스턴스화하기 전에 호출됩니다.

 

Example

class HelloTest : StringSpec({
    lateinit var helloDaoMock: HelloDao
    lateinit var helloService: HelloService
 
    beforeTest {
        helloDaoMock = mockk()
        helloService = HelloService(helloDaoMock)
    }
    ...
}

 


Mocking

  • Kotest 자체에는 모의 기능이 없습니다.
  • 하지만 Mock 라이브러리를 쉽게 플러그인하여 사용할 수 있습니다.
  • 예를 들어 mockk 을 사용하여 다음과 같이 작성할 수 있습니다.
  • mockk: https://mockk.io/
 

MockK

Provides DSL to mock behavior. Built from zero to fit Kotlin language. Supports named parameters, object mocks, coroutines and extension function mocking

mockk.io

testImplementation "io.mockk:mockk:{version}"

 

Example

class MyTest : FunSpec({
 
    val repository = mockk<MyRepository>()
    val target = MyService(repository)
 
    test("Saves to repository") {
        every { repository.save(any()) } just Runs
        target.save(MyDataClass("a"))
        verify(exactly = 1) { repository.save(MyDataClass("a")) }
    }
 
    test("Saves to repository as well") {
        every { repository.save(any()) } just Runs
        target.save(MyDataClass("a"))
        verify(exactly = 1) { repository.save(MyDataClass("a")) }
    }
 
})

 


Matchers

  • Matchers는 Variable 또는 Function이 특정 값을 가져야 한다고 Assert 할 때 사용
  • Kotest에는 100개 이상의 내장 Matcher가 있습니다.

 

Matcher의 Two Styles

  1. Extension functions
  2. Infix functions

 

Example

class MatcherTest: StringSpec({
    "Simple Test"{
    	//haveLength(): length 체크
        "Lorem ipsum dolor" shouldBe haveLength(17)
        
        //include(): 파라미터가 포함되어있는지 체크
        "Lorem ipsum dolor" should include("ipsum")
        
        //endWith(): 파라미터가 끝에 포함되는지 체크
        "Lorem ipsum dolor" should endWith("lor")           
        
        //match(): 파라미터가 매칭되는지 체크
        "Lorem ipsum dolor" should match("Hello World")     
        
        //shouldBeLowerCase(): 소문자로 작성되어있는지 체크
        "Lorem ipsum dolor".shouldBeLowerCase()             
 
        val list = emptyList<String>()                       
        val list2 = listOf("aaa", "bbb", "ccc")
        val map = mapOf<String, String>(Pair("aa", "11"))
        
        //beEmpty(): 원소가 비어있는지 체크
        list should beEmpty()         
        
        //sorted<T>(): 해당 자료형이 정렬되어있는지 체크
        list2 shouldBe sorted<String>()                
        
        //contain(): 해당 원소가 포함되어있는지 체크
        map should contain("aa", "11")       
        
        //haveKey(): 해당 key가 포함되어있는지 체크
        map should haveKey("aa")         
        
        //haveValue(): 해당 value가 포함되었는지 체크
        map should haveValue("11")                          
    }
})

 


Kotest Extension

Kotest는 다른 많은 라이브러리 및 프레임워크와 통합됩니다. 일부는 Kotest 팀에서 제공하고 나머지는 3rd parties에서 유지 관리 및 호스팅합니다.

 


JUnit vs Spock vs Kotest

Ref. https://veluxer62.github.io/explanation/comparing-testing-library-for-kotlin/ 

반응형

댓글