ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Kotlin] Kotest, mockk
    Backend/공부,개념 2023. 9. 3. 17:50
    반응형

    코틀린 스타일 테스트코드

    기존에 사용하던 Junit, AssertJ, Mockito를 이용해서도 테스트코드를 작성할 수 있지만, 중괄호를 활용한 코틀린 dsl 스타일을 적용하기 쉽지 않은데 kotest mockk(코틀린에서 사용하는 mocking라이브러리)를 이용하면 Mocking이나 assertion 과정에서 Kotlin dsl이나 infix를 사용해 조금 더 코틀린스럽게 테스트코드를 작성할 수 있다.

     

     

    Kotest

    코틀린 진영에서 가장 많이 사용되는 테스트 프레임워크

    코틀린 DSL을 활용해 테스트코드를 작성할 수 있다.

    • 다양한 테스트 레이아웃(StringSpec, FunSpec, BehaviorSpec 등 ) 제공
    • Kotlin DSL 스타일의 Assertion 기능 제공
    # kotlin dsl이란
    Domain Specific Language : 특정 도메인에 국한해 사용하는 언어
    
    범용 프로그래밍 언어인 C, C++ 등을 가지고 알고리즘을 푼다.
    SQL언어는 DB (특정영역, 도메인)에 초점을 맞추고 기능을 제한
    
    특징
    - 선언적 문법 ( <-> 명령적 )
    - 깔끔한 코드작성이 가능하다.
      ㄴ 해당 코드가 어떤 로직을 가지고 작업할지 명확하게 이해할 수 있다.
    
    kotlin dsl : https://w97ww.tistory.com/119

     

    Kotest Testing Styles

    보통 이런 모양을 가지고 있다.

    class Tests: XxxSpec({
    	keyword("테스트코드 설명") {
          // keyword는 Spec에 따라서 
          // test, "string", should, describe, given, when, then 등 다양하다.
        }
    
    	xkeyword("테스트 비활성화"){
          // keyword앞에 x가 붙으면 비활성화가 된다.
        }
    })

     

    sample 1) FunSpec

    class MyTests : FunSpec({
        test("addCalculator - 두 파라미터를 더한 값이 반환 된다.") {
        	val result = addCalculator(1, 2)
            result shouldBe 3
        }
    })
    
    
    fun addCalculator(a: Int, b: Int): Int = a + b

     

    test 부분에 테스트의 설명을 적고, 괄호 안 람다 식에 필요한 assertion문을 넣어 테스트를 하면 된다.

    보통 의존성 주입이 필요 없는 도메인 클래스나 도메인 서비스 메소드를 테스트할 때 많이 사용했다.

     

    sample 2) BehaviorSpec

    class MyTests : BehaviorSpec({
        given("a broomstick") { // Given 도 가능 
            and("a witch") { // And
                `when`("The witch sits on it") { // When
                    and("she laughs hysterically") { // And
                        then("She should be able to fly") { // Then
                            // test code
                        }
                    }
                }
            }
        }
    })

    BDD 스타일의 테스트코드를 작성할 때 유용하다. `when`이 예약어라서 사용할 때 \`when`으로 써주어야 해서, 키워드를 대문자로 사용하는 게 편할 수도 있다. And를 이용해서 이어 쓸 수 있고 xGiven, xwhen처럼 x를 붙이면 비활성화할 수 있다.

     

    sample 3) AnnotationSpec

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

    Junit 스타일의 어노테이션 방식을 똑같이 사용할 수 있는 AnnotationSpec도 있다. 

     

    수명주기 / 전후 처리

    BehaviorSpec같은 중첩 테스트에서는 테스트 수명주기를 이해하고 사용하는 것이 좋다.

    Given, When, Then을 만들다 보면 테스트가 중첩으로 생성되기 때문에, 이 수명주기에 따라서 전역으로 관리하거나 mockk 객체를 처리하면 스코프 안에서 얼마나 반복되어 실행되는지, 모킹의 범위가 얼마인지, 어느 시점에 mockk 객체를 초기화해야 될지 등을 알 수 있다.

     

    • beforeContainer, afterContainer
      • Given으로 묶이는 그 아래까지 포함한 영역
    • beforeEach, afterEach
      • Then영역
    • beforeTest, afterTest
    • beforeAny, afterAny
    • beforeSpec, afterSpec
      • 테스트 영역 전체를 Spec이라고 한다
      • XxxSpec을 구현하는 ({}) 영역
    • 등..

     

    Assertion

    io.kotest.matchers 패키지에 종류별로 아주 많이 있다.

     

    • shouldBe, shouldBeNot  
    • shouldHaveSize, shouldContain, ShouldBeGreaterThan ,,
    • shouldThrow<>
    • 등등

    딱히 정해진건 없으니 패키지를 찾아보거나 많이 사용해 보면서 적절한 assertions을 쓰면 될 것 같다. 

     

    예를 들면 list.size() shouldBe 2 보단 list shouldHaveSize 2 를 쓴다거나, 리스트의 아이템을 직접 조회하며 assert문을 쓰지 않고 shouldBeSortedWith(Comparator) 를 쓴다거나,, 

    예외처리에 대한 테스트를 할 때 shouldThrow로 검증을 할 수도 있고, runCatching으로 예외를 잡아서 그 결괏값을 다른 assert문으로 검증하는 것 도 괜찮은 것 같다. 


    Mockk

    kotests와 같이 쓸 수 있는 유용한 모킹 라이브러리이다. jUnit을 사용할 때는 mockito를 주로 썼다면, 코틀린을 쓸 때는 mockk를 쓰면 보통 쓰는 편이다. 

    거의 비슷해서 각자 장/단점을 비교해서 취향껏 선택할 수 있을 정도의 차이인 것 같지만, kotlin dsl 형태로 되어있는 것이나 좀 더 코틀린스러운 테스트코드를 짤 수 있는 느낌이다. 더 자세한 특징은.. 생략 ㅎㅎ

     

    1. 객체 생성

    • val aService = mockk<AService>()
    • val aService: AService = mockk()

    2. Matcher 사용

    참고 링크 : https://mockk.io/#matchers

    every { aService.something() } returns SomeObject()
    
    every { aService.something() } throws Exception()
    
    justRuns { aService.something() } // 결과값이 없는 메소드 단순 호출만 mocking

     

     

    3. 검증

    검증은 보통 kotest의 assertion으로 하거나, verify 메소드를 사용해서 mocking 한 클래스의 특정 메소드가 호출되었는지 동작만을 검증한다.

     

    이때 shouldBe 같은 assert문이 아닌 verify로 메서드가 호출되는지를 검증할 때, 모든 테스트에 걸쳐서 호출 횟수를 세기 때문에 테스트코드의 호출 순서에 따라서 verify문이 실패할 수 있다. (테스트끼리 독립적이지 않다)

    그래서 테스트마다 mock을 clear 하고 다시 mocking을 해줘야 하는데 beforeEach, afterTest 등을 이용해 수명주기에 따라서 해당 동작을 전역적으로 관리할 수 있다.

     

    이 부분 때문에 kotest의 수명주기를 잘 알아두어야 테스트코드를 더 잘 작성할 수 있을 듯하다.

     

     

    mock vs stub

    Test Double
    - 테스트 목적으로 실제 객체 대신 사용되는 모든 종류의 가상 객체

    mock vs stub
    아주 살짝 비교를 해본다면 
    - stub은 실제로 인스턴스화하여 실제로 동작하는 것처럼 만드는 객체
    - mock은 호출에 대한 기대를 명세하고, 그 내용에 따라 동작하도록 프로그래밍된 객체. 

     

    상태 검증 vs 행위 검증
    mock과 stub을 비교할 때 상태 검증, 행위 검증이라는 말을 쓰는데 stub은 메서드가 수행된 뒤에 결과 값의 상태를 확인한다면, mock은 

    실제로 동작을 수행했는지 행위만 검증한다. 

     

    예제 코드

    class AServiceTests : BehaviorSpec({
        val dBService: DBService = mockk()
        
    
        val aService = AService(
                dBService
        )
    
        Given("id가 주어지고") {
            val id = "1"
            When("data가 존재하면") {
                every { dBService.get(id)} returns Data("1", "content")
                aService.request(RequestDto("1", "new content"))
    
                Then("modify가 호출된다.") {
                    verify(exactly = 1) { dBService.modify(any()) }
                    verify(exactly = 0) { dBService.save(any()) }
                }
            }
    
            When("data가 존재하지 않으면") {
                every { dBService.get(id)} returns null
                aService.request(RequestDto("1", "content"))
    
                Then("save가 호출된다.") {
                    verify(exactly = 0) { dBService.modify(any()) }
                    verify(exactly = 1) { dBService.save(any()) }
                }
            }
        }
    
        // 전역적으로 clear, mocking할 만한 메소드들은 수명주기 메소드와 함께 설정
        beforeTest{
            justRun { dBService.modify(any())}
            justRun { dBService.save(any()) }
        }
    
        afterTest {
            clearMocks(dBService)
        }
    })

    요즘에 헥사고날 패턴의 장점을 느낀 건 특정 port를 mocking 해두고 호출되었는지 정도만 검증하면 다른 클래스에 독립적이게 원하는 기능만 테스트할 수 있어서 좋았던 것 같다. 

     

    참고할 링크

    출처

     

     

    댓글

Designed by Tistory.