본문 바로가기
Kotlin

[Kotlin] MockK 사용법 (2) - Mock 객체 선언 방법 (mockk<T>, spyk<T>, spyk(obj))

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

이전 포스팅에서 설명한 MockK 설정이나 간단한 사용법을 알고싶다면 이전 포스팅을 참고하자.

 

이번 포스팅에선 MockK에서 mock 객체를 선언하는 여러 메소드들을 아주 자세하게 알아보려고 한다.

 

테스트 환경 및 방법

 

JUnit 5 + MockK 조합으로 테스트를 작성한다.

 

아래 라이브러리 의존성을 추가해주자.

 

implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.slf4j:slf4j-api:2.0.7")
testImplementation("io.mockk:mockk:1.13.5")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3")
testImplementation("org.slf4j:slf4j-simple:2.0.7")

 

JUnit 5에 MockK를 사용하려면 @ExtendWith(MockKextension::class) 어노테이션을 선언해줘야 한다.

 

@ExtendWith(MockKExtension::class) << 필수
class MockkTest { ... }

 

mockk<T>

 

기본적인 mock 객체를 만들 때 사용하는 메소드

 

아래 UserService로 설명을 하겠다.

 

data class User(
    val id: Long,
    val type: Type,
    val name: String? = "default",
    val address: Address,
    val phones: List<String>,
    val active: Boolean
) {

    data class Address(
        val zipCode: String,
        val basicAddress: String,
        val detailAddress: String
    )

    enum class Type {
        ADMIN,
        USER
    }
}

 

class UserService {

    fun getUser(): User? {
        return User(
            id = 1,
            type = User.Type.ADMIN,
            name = "이름",
            address = User.Address(
                zipCode = "12345",
                basicAddress = "기본 주소",
                detailAddress = "상세 주소"
            ),
            phones = listOf(
                "01012345678",
                "01098765432"
            ),
            active = true
        )
    }

    fun updateUser() {}
}

 

UserService를 mockk를 이용해서 mock 객체로 만들어보자.

 

private val userService = mockk<UserService>()

 

위처럼 작성할 수도 있고 annotation을 붙이는 방법도 있다.

 

@MockK
lateinit var userService: UserService

 

첫 번째 방법을 선호한다. 깔끔하고 라인 수도 더 잡아 먹어서 보기 좋다.

 

파라미터

 

mockk는 name, relaxed, relaxUnitFun, moreInterfaces, block 총 5개의 파라미터를 가지고 있는데 하나씩 알아보자.

 

 

name

 

mockk 객체의 이름을 설정할 때 사용한다.

 

private val userService = mockk<UserService>(name = "userService")

 

mockk 객체의 이름은 여러군데에서 사용되는데 그건 추후에 하나씩 등장할 예정이다.

 

만약, name을 별도로 설정하지 않으면 mockk<클래스명>(번호)로 지어진다. 위 예제에선 mockk<UserService>(1)으로 지어진다.

 

relaxed

 

스터빙 하지 않은 mock 객체의 메소드를 호출할 때 에러를 발생시키지 않는다는 옵션이다. 

 

아래에서 자세히 알아보자.

 

위에서 mock 객체로 선언한 UserService의 getUser() 메소드를 호출해보자.

 

@ExtendWith(MockKExtension::class)
class MockkTest {
    
    private val userService = mockk<UserService>()

    @Test
    fun testMockk() {
        userService.getUser()
    }
}

 

getUser()의 answer 값이 없다고 에러를 뱉는다.

 

no answer found for UserService(#1).getUser() among the configured answers: ()

 

 

그렇다. mockk으로 만든 mock 객체의 메소드들은 별도 스터빙을 하지 않고 호출하면 에러를 발생시킨다.

 

스터빙 하지 않은 메소드를 호출할 때 기본으로 에러를 발생시키는 게 사용하다보면 너무 불편한다.

 

굳이 호출하는 메소드의 리턴 값을 지정해줄 필요없는 경우가 많기 때문이다.

 

이럴 때 relaxed를 true로 주면 된다.

 

private val userService = mockk<UserService>(relaxed = true)

 

relaxed를 붙여서 다시 실행해보자. 에러가 발생하지 않는 걸 볼 수 있다.

 

음 그럼 스터빙을 하지도 않았는데 리턴은 어떤 값이 되는 거지?

 

@ExtendWith(MockKExtension::class)
class MockkTest {

    private val userService = mockk<UserService>(relaxed = true)

    @Test
    fun testMockk() {
        with(userService.getUser()) {
            println("Object: $this")
            println("Long: $id")
            println("Enum: $type")
            println("String: $name")
            println("Object: $address")
            println("List: $phones")
            println("Boolean: $active")
        }
        println("Unit: ${userService.updateUser()}")
    }
}

 

 

Object 기본값들로 채워진 해당 타입의 객체
Long 0
Float, Double 0.0f
Enum 해당 타입의 Enum (ordinal: 0, name: "")
String ""
List, Map 등 빈 컬렉션
Boolean false
Unit Unit

 

Object나 Enum은 child^2 of #1#2 같이 이상한 이름으로 찍힌다.

 

relaxUnitFun

 

리턴 값이 Unit인 fun은 스터빙 하지 않아도 호출 시 에러 발생 시키지 않음

 

relaxed는 리턴 값에 상관없이 전부 적용되는 반면, relaxUnitFun은 리턴 값이 Unit인 메소드에만 적용된다.

 

@ExtendWith(MockKExtension::class)
class MockkTest {

    private val userService = mockk<UserService>(relaxUnitFun = true)

    @Test
    fun testMockk() {
        assertThrows<MockKException> { userService.getUser() }
        assertDoesNotThrow { userService.updateUser() }
    }
}

 

 

리턴 값이 Unit이 아닌 getUser는 에러가 발생하고, 리턴 값이 Unit인 updateUser는 에러가 발생하지 않는다.

 

언제 써야 하는지 아직 감이 잘 오지 않는다.

 

moreInterfaces

 

mock 객체에 interface를 구현 시키는 방법이라는데 정확하게 어떻게 쓰고 어떨 때 사용하는지 모르겠다..

 

찾게되면 다시 업데이트 하겠다.

 

block

 

mock 객체 생성 후 바로 실행될 작업

 

@ExtendWith(MockKExtension::class)
class MockkTest {

    private val userService = mockk<UserService> {
        every { getUser() } returns null
    }

    @Test
    fun testMockk() {
        assertEquals(null, userService.getUser())
    }
}

 

userService가 생성되자마자 getUser()를 null로 반환하라는 스터빙을 선언했다.

 

 

실무에서 저걸 써본 적은 없지만 테스트에서 공통으로 사용하는 스터빙이 있는 경우 사용하면 될 거 같다.

 

spyk<T>, spyk(obj)

 

spy는 스터빙하지 않은 것은 실제 객체와 동일하게 동작한다.

 

mockk은 전부를 mocking하는 반면에, spy는 스터빙하지 않은 것들에 대해선 mocking을 하지 않는다.

 

class Calculator {

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

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

 

위 Calculator를 spy 객체로 만들어보자.

 

private val calculator = spyk<Calculator>()

@SpyK
var calculator: Calculator = Calculator()

 

spyk<T>()나 @SpyK를 사용해서 spy 객체를 만들 수 있다.

 

annotation을 사용하는 방법은 mockk와 조금 다르다. lateinit var를 사용하지 못 한다. var를 사용해야 한다.

 

특이하게 한 가지 방법이 더 존재한다.

 

private val calculator = spyk(Calculator())

 

전자 방법으로 만들면 기본 생성자를 사용한다.

 

후자 방법은 다른 생성자를 사용하고 싶거나 생성자에 필요한 값들을 넣고 싶을 때 사용하면 된다.

 

@ExtendWith(MockKExtension::class)
class SpyKTest {

    private val calculator = spyk<Calculator>()

    @Test
    fun testSpyK() {
        assertEquals(3, calculator.plus(1, 2))
    }
}

 

spy 객체로 만든 caculator의 plus를 호출해보자.

 

어떻게 될까?

 

 

성공이다.

 

spy 객체는 별도 스터빙을 하지 않아도 mockk 처럼 에러가 발생하지 않는다.

 

파라미터

 

spyk는 name, moreInterfaces, recordPrivateCalls, block 총 4개 파라미터를 가지고 있다.

(spyk(obj)는 obj 파라미터가 하나 더 있다.)

 

mockk에서 설명한 파라미터들은 설명을 생략한다.

 

 

recordPrivateCalls

 

이름 그대로 private 메소드 호출을 record 할거냐는 의미이다.

 

true로 주면 private 메소드를 verify 할 수 있다.

 

private val calculator = spyk<Calculator>(recordPrivateCalls = true)

 

private method 스터빙 방법은 특이하다. 

 

private 메소드 signature를 반드시 맞춰줘야 한다.

 

every { spykObject["private 메소드명"](private 메소드 signature) } returns 값

 

실제 private 메소드가 1번 호출됐는지 verify 해보자.

 

@ExtendWith(MockKExtension::class)
class SpyKTest {

    private val calculator = spyk<Calculator>(recordPrivateCalls = true)

    @Test
    fun testSpyK() {
        every { calculator["privatePlus"](1, 2) } returns 50

        assertEquals(50, calculator.plus(1, 2))
        
        verify(exactly = 1) { calculator["privatePlus"](1, 2) }
    }
}

 

 

privatePlus 메소드가 1번 호출된 게 검증 잘 됐다.

 

만약, recordPrivateCalls를 설정 안 하고 private 메소드를 스터빙하면?

 

@ExtendWith(MockKExtension::class)
class SpyKTest {

    private val calculator = spyk<Calculator>(recordPrivateCalls = false)

    @Test
    fun testSpyK() {
        every { calculator["privatePlus"](1, 2) } returns 50

        assertEquals(50, calculator.plus(1, 2))

        verify(exactly = 1) { calculator["privatePlus"](1, 2) }
    }
}

 

 

호출되지 않았다고 에러가 발생한다.

 

mockk는 어떻게 되나 테스트를 해봤는데?

 

@ExtendWith(MockKExtension::class)
class MockkWithPrivateTest {

    private val calculator = mockk<Calculator>(relaxed = true)

    @Test
    fun testMockkWithPrivate() {
        every { calculator.plus(any(), any()) } answers { callOriginal() } // 실제 작동하게 스터빙
        every { calculator["privatePlus"](1, 2) } returns 50

        assertEquals(50, calculator.plus(1, 2))

        verify(exactly = 1) { calculator["privatePlus"](1, 2) }
    }
}

 

 

? 성공했다. 뭐지..

 

mockk는 private 메소드도 기본적으로 record하고 있는 걸로 보인다.

 

callOriginal을 이용해서 spy 객체처럼 작동하게 했더니 이게 되네..;;

 

 

Mock 객체 생성 메소드를 한번에 다 정리하려고 했는데 mockk하고 spyk 2개 정리했더니 너무 길어져서 다음 포스팅에 mockkClass, mockkObject, mockkStatic, mockkConstructor를 정리하는 걸로...

반응형

댓글