프로그래밍 노트/TEST를 해보자

[Kotest] kotest, mockk 사용하기

깡냉쓰 2023. 10. 25. 21:56
728x90
반응형

setup

  1. dependency 추가
tasks.withType<Test>().configureEach {
   useJUnitPlatform()
}

// Test Framework
testImplementation('io.kotest:kotest-runner-junit5:5.5.4')
// Assertions Library
testImplementation('io.kotest:kotest-assertions-core:5.5.4')
  1. Kotest intellij plugin 설치

Testing Style

  • kotest는 10개의 레이아웃을 제공하며, 이 중 하나를 상속받아서 사용 가능
  • 여러 테스트 프레임워크에서 영감을 받아 작성된 레이아웃도 존재

사용하면 편할 것 같은 레이아웃을 몇 개 살펴보면

  • Should Spec
  • Behavior Spec
  • Free Spec
  • Annotation Spec(Junit 스펙과 비슷하여 가져와봄)

Should Spec

  • 테스트 자체를 람다로 호출하여 테스트를 생성 가능
class SampleShouldSpec: ShouldSpec({
    should("return the length of the string") {
        "dcb".length shouldBe 3
        "".length shouldBe 0
    }

    // junit5 @Nested 
    context("String Test") {
        should("return the length of the string") {
            "dcb".length shouldBe 3
            "".length shouldBe 0
        }
        should("xxx") {
            // testCode
        }
    }
    context("BigDecimal Test") {
        should("return the scale of the number") {
            BigDecimal("300.00").scale() shouldBe 2
            BigDecimal("300.0").scale() shouldBe 1
        }
    }

    // junit @Ignore과 동일한 효과 (x를 앞에 붙인다.)
    xcontext("this block is disabled") {
        should("disabled by inheritance from the parent") {
            // test here
        }
    }
})

Behavior Spec

  • BDD 스타일
class SampleBehaviorSpec: BehaviorSpec({
    val calculator = Calculator()
    given("calculator") {
        `when`("10과 5를 더하면") {
            val result = calculator.add(10, 5)
            then("15가 반환된다.") {
                result shouldBe 15
            }
        }
        `when`("null 을 입력하는 경우") {
            then("IllegalArgumentException 을 던진다.") {
                shouldThrow<IllegalArgumentException> {
                    calculator.add(null, 5)
                }
            }
        }
    }
})

Free Spec

  • - 키워드로 뎁스(Grouping)를 지정
  • 가장 많이 사용될 것으로 예상
class SampleFreeSpec: FreeSpec({
    "calculator" - {
        val calculator = Calculator()
        "10과 5를 더하면 15가 반환된다." {
            calculator.add(10, 5) shouldBe 15
        }
        "null을 입력하는 경우 IllegalArgumentException을 던진다." {
            shouldThrow<IllegalArgumentException> {
                calculator.add(null, 5)
            }
        }
    }
})

Annotation Spec

  • Junit4/5 스펙과 비슷하게 테스트코드를 작성할 수 있음
  • Junit을 사용하는 것에 비해 큰 이점을 제공하지 않으며, 마이그레이션할때 주로 이용하는 것으로 보임
class SampleAnnotationSpec: AnnotationSpec() {
    @BeforeEach
    fun beforeEach() {
        println("Before Each")
    }

    @Test
    fun test() {
        1 shouldBe 1
    }
}

Lifecycle hook

class LifeCycleHookTest: FreeSpec({
    beforeSpec() {
        println("Before Spec")
    }

    beforeContainer() {
        println("Before Container")
    }

    beforeEach() {
        println("Before Each")
    }

    beforeAny() {
        println("Before Any")
    }

    beforeTest() {
        println("Before Test")
    }

    "컨테이너1" - {
        println("컨테이너 1")
        "테스트1" {
            println("테스트1")
        }
        "테스트2" {
            println("테스트2")
        }
    }
})
Before Spec

Before Container
Before Any
Before Test
컨테이너 1

Before Each
Before Any
Before Test
테스트1

Before Each
Before Any
Before Test
테스트2

Assertion

"Matchers" - {
    "General" {
        "Hello Kotlin!" shouldBe "Hello Kotlin!"
        true.shouldBeTrue()
        shouldThrow<ArithmeticException> {
            1 / 0
        }
    }

     "Maps" {
         val map = mapOf(1 to "one", 2 to "two", 7 to "seven")
         map shouldContainKey  1
         map shouldContainValue "one"
         map shouldContain Pair(1, "one")
     }

    "Strings" {
        " ".shouldBeBlank()
        "".shouldBeEmpty()
        "Hello Kotlin" shouldContain "Kotlin"
        "Hello Kotlin" shouldHaveLength 12
        "12345Hello Kotlin".shouldContainADigit()
        "12345Hello Kotlin" shouldEndWith("Kotlin")
        "12345Hello Kotlin" shouldStartWith "12345"
    }

    "Integers" {
        5 shouldBeLessThan 6
        7 shouldBeGreaterThan 6
        5 shouldBeInRange IntRange(1, 7)
    }

    "Collections" {
        val list = listOf(1, 2, 3, 4, 5)
        list shouldContain 1
        list shouldContainAll listOf(1,2,3)
    }
}

Data Driven Testing

testImplementation("io.kotest:kotest-framework-datatest:5.5.4")
"simple" - {
    val list = listOf("1", "2", "300", "500")
    withData(list) { str ->
        str.shouldContainADigit()
    }
}

// 중첩 가능
"nested data test" - {
    val services = listOf(
        "http://internal.foo",
        "http://internal.bar",
        "http://public.baz",
    )

    val methods = listOf("GET", "POST", "PUT")

    withData(services) { service ->
        withData(methods) { method ->
            // test service against method
        }
    }
}

  • 공식사이트에서는 depth가 이쁘게 나오는데, 실제 intellij 에서 테스트하면 위처럼 나오지 않네요,

Mockk

  • 코틀린 스타일의 Mock 프레임워크
  • Mockito에서 지원하는 모든 기능에 몇몇 추가 기능을 더한 Kotlin을 위한 모킹 프레임워크
  • https://mockk.io/
testImplementation("io.mockk:mockk:1.13.4")
"MockkTest" {
    // Mock 객체 생성 (Spy 객체는 spyk로 생성)
    // 1. mockk<타입>()
    val userRepository = mockk<UserRepository>()
    // 2. 타입 추론
    // val userRepository: UserRepository = mockk()

        // Relaxed mock (mocking 하지 않은 메소드를 호출하여도 예외가 발생하지 않음)
    // val userRepository = mockk<UserRepository>(relaxed = true)


    // mocking
    every { userRepository.save(any()) } returns User("sam", 30)
    every { userRepository.save(User("tom", 20)) } throws IllegalArgumentException()

    userRepository.save(User("tony", 29)) shouldBe User("sam", 30)
    shouldThrow<java.lang.IllegalArgumentException> {
        userRepository.save(User("tom", 20))
    }

    // verification
    verifyAll {
        userRepository.save(User("tony", 29))
        userRepository.save(User("tom", 20))
    }

    verify(exactly = 1) { userRepository.save(User("tony", 29)) }
    verify(atLeast = 1) { userRepository.save(User("tony", 29)) }
    verify(atMost = 1) { userRepository.save(User("tony", 29)) }
}
testImplementation("com.ninja-squad:springmockk:2.0.3")
implementation("io.kotest.extensions:kotest-extensions-spring:1.1.2")
@SpringBootTest
class GreetingControllerTest() : FreeSpec() {

    // register SpringExtension
    override fun extensions() = listOf(SpringExtension)

    @Autowired
    lateinit var controller: UserController

    @MockkBean
    lateinit var service: UserService

    init {
        "mockkBean 사용" - {
            every { service.greet("John") } returns "Hi John"

            controller.greet("John") shouldBe "Hi John"
            verify { service.greet("John") }
        }
    }
}

참고)

728x90
반응형