コドモン Product Team Blog

株式会社コドモンの開発チームで運営しているブログです。エンジニアやPdMメンバーが、プロダクトや技術やチームについて発信します!

コードサンプルで学ぶMockKの使い方

こちらは「コドモン Advent Calendar 2023」の 7日目の記事です

qiita.com

こんにちは。プロダクト開発部の上代です。

私のチームではKotestを使用してテストコードを書いており、モッキングライブラリのMockKを採用しています。 MockKはKotlin専用のモッキングライブラリのため、モックの作成と管理がとてもやりやすいと思っています。 今回はコードサンプルを用いて、MockKの使い方を紹介していきます。

前提条件

バージョン

  • Kotlin 1.8.21
  • MockK 1.13.8

テスト対象

テスト対象として以下のコードを用意しました。

interface UserRepository {
    fun getUser(userId: String): User
    fun updateUser(userId: String, user: User): Boolean
    suspend fun awaitUpdateUser(userId: String, user: User): Boolean
}

class UserService(private val userRepository: UserRepository) {
    fun getUser(userId: String): User {
        return userRepository.getUser(userId)
    }

    fun updateUser(userId: String, user: User): Boolean {
        return userRepository.updateUser(userId, user)
    }

    suspend fun awaitUpdateUser(userId: String, user: User): Boolean {
        return userRepository.awaitUpdateUser(userId, user)
    }
}

data class User(val userId: String, val name: String, val email: String)

ユーザーサービスがユーザーリポジトリを使用してユーザー情報を取得したり、更新したりするコードです。 このコードに対して3つのテストを書いてみました。

テストコード

import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.shouldBe
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import kotlinx.coroutines.runBlocking

class UserServiceTest : DescribeSpec({

    val mockUserRepo = mockk<UserRepository>()
    val userService = UserService(mockUserRepo)

    describe("UserService") {
        it("returns a user for getUser") {
            val userId = "user123"
            val expectedUser = User(userId, "Test User", "test@user.com")
            every { mockUserRepo.getUser(userId) } returns expectedUser

            val resultUser = userService.getUser(userId)

            resultUser shouldBe expectedUser
            verify(exactly = 1) { mockUserRepo.getUser(userId) }
        }

        it("throws an exception for getUser when user not found") {
            val userId = "user999"
            every { mockUserRepo.getUser(userId) } throws NoSuchElementException("User not found")

            val exception = shouldThrowAny { userService.getUser(userId) }

            exception.message shouldBe "User not found"
            verify(exactly = 1) { mockUserRepo.getUser(userId) }
        }

        it("updates a user correctly for updateUser") {
            val userId = "user123"
            val newUserDetails = User(userId, "Updated User", "update@user.com")
            val userSlot = slot<User>()
            every { mockUserRepo.updateUser(eq(userId), capture(userSlot)) } answers {
                userSlot.captured.email == newUserDetails.email && userSlot.captured.name == newUserDetails.name
            }

            val isUpdated = userService.updateUser(userId, newUserDetails)

            isUpdated shouldBe true
            userSlot.captured.name shouldBe newUserDetails.name
            userSlot.captured.email shouldBe newUserDetails.email
            verify { mockUserRepo.updateUser(eq(userId), any()) }
        }

        describe("suspend functions") {
            it("updates a user correctly with awaitUpdateUser") {
                val userId = "user123"
                val newUser = User(userId, "Async User", "async@update.com")
                coEvery { mockUserRepo.awaitUpdateUser(userId, newUser) } returns true

                val result = runBlocking { userService.awaitUpdateUser(userId, newUser) }

                result shouldBe true
                coVerify(exactly = 1) { mockUserRepo.awaitUpdateUser(userId, newUser) }
            }
        }
    }
})

解説

テストを1つずつ解説していきます。

モックの作成

UserRepositoryのモックを作成してUserServiceのコンストラクタに渡しています。

val mockUserRepo = mockk<UserRepository>()
val userService = UserService(mockUserRepo)

1つ目のテスト

it("returns a user for getUser") {
    val userId = "user123"
    val expectedUser = User(userId, "Test User", "test@user.com")

    every { mockUserRepo.getUser(userId) } returns expectedUser

    val resultUser = userService.getUser(userId)

    resultUser shouldBe expectedUser

    verify(exactly = 1) { mockUserRepo.getUser(userId) }
}

everyはモックされたオブジェクトの特定のメソッド呼び出しに対する振る舞いを定義します。 ここではgetUserの振る舞いを明示的に定義しています。

every { mockUserRepo.getUser(userId) } returns expectedUser

verify関数はモックオブジェクトのメソッド呼び出しを検証します。 ここではgetUserが1回呼び出されたことを検証しています。

verify(exactly = 1) { mockUserRepo.getUser(userId) }

2つ目のテスト

it("throws an exception for getUser when user not found") {
    val userId = "user999"
    every { mockUserRepo.getUser(userId) } throws NoSuchElementException("User not found")

    val exception = shouldThrowAny { userService.getUser(userId) }

    exception.message shouldBe "User not found"
    verify(exactly = 1) { mockUserRepo.getUser(userId) }
}

everyで例外を発生させることも可能です。

every { mockUserRepo.getUser(userId) } throws NoSuchElementException("User not found")

3つ目のテスト

it("updates a user correctly for updateUser") {
    val userId = "user123"
    val newUserDetails = User(userId, "Updated User", "update@user.com")
    val userSlot = slot<User>()
    every { mockUserRepo.updateUser(eq(userId), capture(userSlot)) } answers {
        userSlot.captured.email == newUserDetails.email && userSlot.captured.name == newUserDetails.name
    }

    val isUpdated = userService.updateUser(userId, newUserDetails)

    isUpdated shouldBe true
    userSlot.captured.name shouldBe newUserDetails.name
    userSlot.captured.email shouldBe newUserDetails.email
    verify { mockUserRepo.updateUser(eq(userId), any()) }
}

slotはモックしたメソッドに渡される引数をキャプチャすることができます。 Userタイプの引数をキャプチャするためのslotを作成しています。

val userSlot = slot<User>()

capture(userSlot)で、メソッドの第二引数として渡されるUserオブジェクトをキャプチャするために使用されます。

every { mockUserRepo.updateUser(eq(userId), capture(userSlot)) } answers {
    userSlot.captured.email == newUserDetails.email && userSlot.captured.name == newUserDetails.name
}

updateUserの第二引数として渡されるUserオブジェクトのnameとemailの値を検証しています。

userSlot.captured.name shouldBe newUserDetails.name
userSlot.captured.email shouldBe newUserDetails.email

4つ目のテスト

it("updates a user correctly with awaitUpdateUser") {
    val userId = "user123"
    val newUser = User(userId, "Async User", "async@update.com")
    coEvery { mockUserRepo.awaitUpdateUser(userId, newUser) } returns true

    val result = runBlocking { userService.awaitUpdateUser(userId, newUser) }

    result shouldBe true
    coVerify(exactly = 1) { mockUserRepo.awaitUpdateUser(userId, newUser) }
}

Kotlinでコルーチンを用いた非同期処理を行う場合、標準のeveryブロックではなく、coEveryを使用する必要があります。

coEvery { mockUserRepo.awaitUpdateUser(userId, newUser) } returns true

Kotlinでコルーチンを用いた非同期処理を行う場合、標準のverifyブロックではなく、coVerifyを使用する必要があります。

coVerify(exactly = 1) { mockUserRepo.awaitUpdateUser(userId, newUser) }

さいごに

MockKの使い方を紹介しました。 さらに詳しい使い方を知りたい方は、MockKのドキュメントを参照してみてください。