본문 바로가기
Kotlin

[Kotlin] MockK 사용법 (3) - Mock 객체 선언 방법 (mockkClass, mockkObject, mockkStatic, mockkConstructor)

by 노력남자 2023. 5. 15.
반응형

이번 포스팅에선 이전 포스팅에서 끝내지 못한 Mock 객체 선언 방법을 이어서 정리하려고 한다.

 

mockkClass

 

클래스를 기반으로 mock 객체를 만들 때 사용한다.

 

mockk는 제네릭을 사용하는 반면 mockkClass는 Class를 사용한다.

 

// mockkClass
private val userService = mockkClass(UserService::class)

// mockk
private val userService = mockk<UserService>()

 

사용방법은 mockk와 동일하다.

 

@Test
fun mockkTest() {
    val userService = mockkClass(UserService::class)

    every { userService.getUser() } returns null

    assertNull(userService.getUser())
}

 

파라미터도 동일하다.

 

 

mockk로만으로도 충분한 거 같은데 mockkClass의 존재 이유를 잘 모르겠다.

 

알게되면 추가하겠다.

 

mockkObject

 

mockkObject는 enum, object 를 mock 객체로 만들 때 사용한다.

 

mockkObject를 스터빙하면 다른 테스트들에 영향이가니 꼭 unmock을 해줘야 한다. (자세한 건 여기서!)

 

mockkObject(Object)

 

mockkObject(Object)로 mock 객체로 만들 수 있다.

 

이전에 소개했던 것들과는 모양이 다르게 생겼다.

 

object 형태들을 대상으로 하는 메소드라 리턴 값이 Unit이다.

 

별도 스터빙을 하지 않으면 spy처럼 기존 로직을 실행한다.

 

enum

 

enum class Membership(
    val label: String
) {
    BRONZE("브론즈"),
    SILVER("실버"),
    GOLD("골드");
}

 

Membership enum class를 mock 객체로 선언해보겠다.

 

@Test
fun testEnum() {
    mockkObject(Membership.BRONZE)

    every { Membership.BRONZE.label } returns  "골드"

    assertEquals("골드", Membership.BRONZE.label)
}

 

잘 스터빙이 된 걸 볼 수 있다.

 

 

object

 

object Calculator {

    fun plus(a: Int, b: Int): Int {
        return privatePlus(a, b)
    }

    private fun privatePlus(a: Int, b: Int): Int {
        return a + b
    }
}

 

이전에 사용했던 Calculator를 object로 만들어 테스트해보겠다.

 

@Test
fun testObject() {
    mockkObject(Calculator)

    every { Calculator.plus(any(), any()) } returns 1000

    assertEquals(1000, Calculator.plus(1, 2))
}

 

잘 스터빙이 된 걸 볼 수 있다.

 

 

혹시나 해서 mockk로도 object를 만들 수 있을까 테스트를 해봤는데

 

@Test
fun testObjectUsingMockk() {
    val calculator = mockk<Calculator>()

    every { calculator.plus(any(), any()) } returns 1000

    assertEquals(1000, calculator.plus(1, 2))
}

 

mockk를 사용하면 변수로 선언을 해야하기 때문에 object class에 맞지 않는다.

 

 

근데 mock 객체로 선언되어 스터빙이 제대로 되긴 하는데 너무 어거지로 해야한다.

 

MockK를 써보면 어거지로 하면 되긴 하는데 그러지말고 용도에 맞는 걸 잘 찾아쓰는 게 중요한 거 같다.

 

파라미터

 

 

objects(vararg였군..)와 recordPrivateCalls 2개 있다.

 

이전에 알아본 파라미터라 지나가겠다.

 

mockkStatic

 

java static method, top level function, extension을 mock 객체로 선언할 때 사용한다.

 

mockkStatic을 스터빙하면 다른 테스트들에 영향이가니 꼭 unmock을 해줘야 한다. (자세한 건 여기서!)

 

mockkStatic(StaticObject)

 

별도 스터빙을 하지 않으면 spy처럼 기존 로직을 실행한다.

 

java static method

 

코틀린으로 구현된 메소드만 사용하면 좋겠지만 어쩔 수 없이 자바로 구현된 걸 사용해야 하는 경우가 많다.

 

java static method를 스터빙하려면 static 메소드를 가지고 있는 클래스를 mockkStatic으로 mock 객체로 만들어야 한다.

 

 

우리가 아주 자주 사용하는 LocalDateTime.now()는 java static method다.

 

이걸 스터빙해보자.

 

@Test
fun testJavaStaticMethod() {
    mockkStatic(LocalDateTime::class)

    every { LocalDateTime.now() } returns LocalDateTime.of(2023, 5, 3, 0, 0, 0)

    assertEquals(LocalDateTime.of(2023, 5, 3, 0, 0, 0), LocalDateTime.now())
}

 

스터빙이 잘 된 걸 볼 수 있다.

 

 

top level function

 

fun helloWorld(): String {
    return "hello world"
}

 

helloWorld top level function을 mock 객체로 만들어보자.

 

@Test
fun testTopLevelFun() {
    mockkStatic(::helloWorld)

    every { helloWorld() } returns "yeah"

    assertEquals("yeah", helloWorld())
}

 

스터빙이 잘 된 걸 볼 수 있다.

 

 

extension

 

extension은 class-wide, object-wide, module-wide 총 3개가 있는데

 

class-wide, object-wide는 mockk로 가능하다.

 

data class Obj(val value: Int)

class Ext {
    fun Obj.extensionFunc() = value + 5
}
@Test
fun testClassExtension() {
    with(mockk<Ext>()) {
        every { Obj(5).extensionFunc() } returns 11

        assertEquals(11, Obj(5).extensionFunc())
    }
}

 

 

근데 module-wide는 mockkStatic를 써야한다.

 

package com.example.mockk

fun helloWorld(): String {
    return "hello world"
}

fun String.capitalizeWords(): String {
    val words = this.split(" ")
    val capitalizedWords = words.map { it.capitalize() }
    return capitalizedWords.joinToString(" ")
}

 

helloWord top level function, String.capitalizeWords extension을 com.example.mockk.TopLevelFunction.Kt 파일에 선언해놨다.

 

String.capitalizeWords를 mock 객체로 선언해보자.

 

두 가지 방법이 있다.

 

첫 번째는 package 명 + 파일명 + 확장자까지 해서 통째로 mock 객체로 선언하는 방법이다.

 

@Test
fun testModuleExtension1() {
    mockkStatic("com.example.mockk.TopLevelFunctionKt")

    every { "test".capitalizeWords() } returns "test"

    assertEquals("test", "test".capitalizeWords() )

    every { helloWorld() } returns "yeah"

    assertEquals("yeah", helloWorld())
}

 

두 번째는 아까 위에서 top level function에서 설명한 거처럼 쓸 수 있다.

 

@Test
fun testModuleExtension2() {
    mockkStatic(String::capitalizeWords)

    every { "test".capitalizeWords() } returns "test"

    assertEquals("test", "test".capitalizeWords() )

    every { helloWorld() } returns "yeah"

    assertEquals("yeah", helloWorld())
}

 

mockkStatic("com.example.mockk.TopLevelFunctionKt"), mockkStatic(String::capitalizeWords) 두 방법 모두 파일 안에 있는 것들 전부를 mock 객체로 선언한다.

 

파라미터

 

 

별거없다.

 

mockkConstructor

 

생성자를 mock 객체로 선언할 때 사용한다.

 

mockkConstructor를 스터빙하면 다른 테스트들에 영향이가니 꼭 unmock을 해줘야 한다. (자세한 건 여기서!)

 

mockkConstructor(MockCls::class)

 

선언 방법은 이전 것들과 동일하다.

 

스터빙 하는 방법이 좀 다르다.

 

every { constructedWith<MockCls>().add(1) } returns 2

 

위 형태처럼 constructedWith<Class>(인자)로 사용해야 한다.

 

constructedWith를 사용하면 명시한 생성자별로 prototype mock이라는 걸로 생성이 된다.

 

prototype mock별로 호출이 record된다. 이게 무슨 소린지 아래서 알아보자.

 

MockCls의 생성자를 mock 객체로 만들어보자.

 

class MockCls(private val a: Int = 0) {
    constructor() : this(2)
    constructor(x: String) : this(x.toInt())
    fun add(b: Int) = a + b
}

 

생성자가 총 3개인 클래스다. 전부 테스트를 해보자.

 

@Test
fun testConstructor() {
    mockkConstructor(MockCls::class)

    every { constructedWith<MockCls>().add(1) } returns 2
    every { constructedWith<MockCls>(OfTypeMatcher<String>(String::class)).add(2) } returns 3
    every { constructedWith<MockCls>(EqMatcher(4)).add(any()) } returns 4

    assertEquals(2, MockCls().add(1))
    assertEquals(3, MockCls("2").add(2))
    assertEquals(4, MockCls(4).add(7))

    verify {
        constructedWith<MockCls>().add(1)
        constructedWith<MockCls>(OfTypeMatcher<String>(String::class)).add(2)
        constructedWith<MockCls>(EqMatcher(4)).add(7)
    }
}

 

constructedWith<MockCls>(인자)로 스터빙을 하면 어디에 따로 명시하지 않아도 mock 객체가 생성되는데 이걸 prototype mock이라고 하는 것 같다.

 

그래서 아래 verify에 호출된 걸 각 prototype mock을 이용해 검증할 수 있다.

 

파라미터

 

 

총 3개의 파라미터가 있는데 localToThread는 처음본다.

 

자세한 사용방법을 잘 모르겠어서 찾아보고 추가하겠다.

반응형

댓글