虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

Kotlinのモックライブラリ「MockK」- 使い方と便利レシピのご紹介!

はじめに

こんにちは!虎の穴ラボの鷺山です。

今回は、虎の穴ラボの推し言語であるKotlinのモックライブラリMockKについて、基本的な使い方のほか、実践的な使い方のいくつかを「レシピ集」としてご紹介したいと思います!

MockKとは?

MockKは、Kotlin向けのテスト用モックライブラリで、ライブラリ自体もKotlinで作られています。
Kotlin向けであるため、Kotlin特有の言語仕様を広くカバーしています。このため、Java向けのモックライブラリに比べてより直感的にKotlinのコードのモックを書ける点が特徴です。

前提環境

言語・ライブラリ バージョン
Kotlin 1.9.10
JUnit 5.10.0
MockK 1.13.7

導入方法

GradleプロジェクトにMockKの依存関係を追加します。

▼build.gradle.kts

dependencies {
    testImplementation("io.mockk:mockk:1.13.7")
}

基本的な使い方

使い方の例として、以下の Adder クラスの add() メソッドのテストを書きます。

class Adder(
    private val value1: IntValue,
    private val value2: IntValue,
) {
    // テスト対象のメソッド
    fun add(): Int {
        return value1.getInt() + value2.getInt()
    }
}

上記の Adder クラスは、以下の IntValue クラスに依存しています。今回はこの IntValue クラスや getInt() メソッドをMockKを使ってモック化します。

// モックするクラス
class IntValue {
    // モックするメソッド
    fun getInt(): Int {
        return 0 // 本番コードでは 0 を返している
    }
}

MockKで IntValue をモック化した Adder のテストコードは以下のようになります:

class AdderTest {
    private val value1Mock = mockk<IntValue>()
    private val value2Mock = mockk<IntValue>()

    // テスト対象 (System Under Test)
    private val sut = Adder(value1Mock, value2Mock)

    @Test
    fun `add()はvalue1とvalue2の和を返す`() {
        // 準備
        every { value1Mock.getInt() } returns 2
        every { value2Mock.getInt() } returns 3

        // 実行
        val result = sut.add()

        // 検証
        assertEquals(5, result)
    }
}
  • mockk<モックするクラス>() でモックオブジェクトを作成します。
  • every{ モックオブジェクト.モックするメソッド() } returns 返却値 で、モックオブジェクトのメソッドの挙動を設定します。
    • (この例ではメソッドに引数がないため該当しませんが)モックするメソッドには引数を指定できます。
      • 引数に具体的な値を指定すると、その値が渡された場合のみ該当のモックが有効になります。
      • 引数に any() を指定すると、どんな値が渡されても該当のモックが有効になります。

基本的な使い方としては以上を抑えておけばOKです。
次のセクションから、実際の活用方法に焦点を当てて、いくつかの便利なレシピをご紹介いたします。

レシピ集

1. モックの挙動を呼ばれるごとに変化させる

たとえばIDの採番処理など、「同じ引数を取るが、呼び出すたびに異なる値を返す」ようなメソッドをモックする場合は、以下のように書くことができます。

// モックするクラスとメソッド
interface IdGenerator {
    fun nextId(): Long
}
// テストコード
val idGeneratorMock = mockk<IdGenerator>()
every { idGeneratorMock.nextId() } returns 1001 andThen 1002 andThen 1003
  • andThen を続けることで、呼ばれるごとに異なる値を返すことができます。

2. モック実行時の引数を検証する

「モックのメソッドがどんな引数で呼ばれたか」を検証したいケースはあると思います。
やり方は色々ありますが、capture() を使う方法が簡単で分かりやすいのでご紹介します。

▼本番コード

// とあるデータクラス
data class Record(val key: Long, val value: String)

// モックするクラスとメソッド
interface Database {
    fun insert(record: Record)
}

// テスト対象のクラスとメソッド
class RecordRepository(private val database: Database) {
    fun save(keyValuePairs: List<Pair<Long, String>>) {
        keyValuePairs
            .map { Record(it.first, it.second) }
            .forEach { database.insert(it) }
    }
}

▼テストコード

class RecordRepositoryTest {
    private val databaseMock = mockk<Database>()
    private val sut = RecordRepository(databaseMock)

    @Test
    fun `save()はキーと値のリストをRecord化してinsertする`() {
        // パラメータ準備
        val keyValues = listOf(
            1001L to "hoge",
            1002L to "fuga",
            1003L to "piyo",
        )

        // モック準備
        val captured = mutableListOf<Record>()
        justRun {
            databaseMock.insert(capture(captured))
        }

        // 実行
        sut.save(keyValues)

        // 検証
        val expected = listOf(
            Record(1001L, "hoge"),
            Record(1002L, "fuga"),
            Record(1003L, "piyo"),
        )
        assertEquals(expected, captured)
    }
}
  • capture() を使うことで、モック実行時の引数を記録できます。

    • 記録された引数は、captured リストに蓄積されます。
      • captured はミュータブルなリストで宣言しておく必要があります。
  • また capture() を使うことで、上記のように引数を返さないvoidメソッドに対するテストも書くことができます。

    • なおvoidメソッドの場合は、every {...} ではなく justRun {...} でモックの挙動を設定します。

3. Object や Companion Object のメソッドをモックする

Kotlin独自の言語仕様としてObjectやCompanion objectが存在しますが、MockKではこれらもモック化することが可能です。

▼本番コード

object SomeObject {
    fun method(): String = "AAA"
}

class SomeClass {
    companion object {
        fun method(): String = "BBB"
    }
}

▼テストコード

mockkObject(SomeObject)
mockkObject(SomeClass)

every { SomeObject.method() } returns "YYY"
every { SomeClass.method() } returns "ZZZ"

println(SomeObject.method()) // AAA ではなく YYY が返る
println(SomeClass.method()) // BBB ではなく ZZZ が返る
  • どちらも mockkObject() を使ってモック化できます。モックの挙動は every {...} を使って設定します。

4. コンストラクタをモックする

コンストラクタによるインスタンス生成もMockKでモック化することが可能です。

▼本番コード

class Adder {
    private val value1 = IntValue() // インスタンス生成が行われている
    private val value2 = IntValue()

    // テスト対象のメソッド
    fun add(): Int {
        return value1.getInt() + value2.getInt()
    }
}

// モックするクラスやメソッド
class IntValue {
    fun getInt(): Int = 0 // 本番コードでは 0 を返している
}

▼テストコード

// 準備
mockkConstructor(IntValue::class)
every { anyConstructed<IntValue>().getInt() } returns 2 andThen 3

// 実行
val result = Adder().add()

// 検証
assertEquals(5, result)
  • mockkConstructor(モックするクラス::class) と記述することで、そのクラスのコンストラクタをモック化の対象にすることができます。
  • every { anyConstructed<モックするクラス>().モックするメソッド() } returns 返却値 と記述することで、該当クラスのコンストラクタから生成されたインスタンスのメソッドをモック化することができます。

5. 現在の時刻をモックする

通常、「現在の時刻」はテストの実行時に毎回変わってしまうものですが、これをモック化することで値を固定化し、検証を容易にすることができます。

▼本番コード

class CurrentTime {
    fun getCurrentTime(): String {
        return "現在の時刻は ${LocalDateTime.now()} です"
    }
}

▼テストコード

// 準備
mockkStatic(LocalDateTime::class)
every { LocalDateTime.now() } returns LocalDateTime.parse("2024-12-31T11:22:33.444")

// 実行
val result = CurrentTime().getCurrentTime()

// 検証
val expected = "現在の時刻は 2024-12-31T11:22:33.444 です"
assertEquals(expected, result)
  • mockkStatic() を使用することで、Java由来のstaticメソッドもモック化できます。

注意点1: System.currentTimeMillis() は直接モックできない

既知のIssueとして System.currentTimeMillis() は直接モック化することができません。

ワークアラウンドとして、ラップするメソッドを間に挟むことでこの問題の回避が可能です。例をご紹介します:

▼本番コード

object EpochMillis {
    // System.currentTimeMillis() をラップ
    fun now() = System.currentTimeMillis()
}

class CurrentTime {
    fun getCurrentEpoch(): String {
        return "現在のエポックミリ秒は ${EpochMillis.now()} です"
    }
}

▼テストコード

// 準備
mockkObject(EpochMillis)
every { EpochMillis.now() } returns 1700000000000L

// 実行
val result = CurrentTime().getCurrentEpoch()

// 検証
val expected = "現在のエポックミリ秒は 1700000000000 です"
assertEquals(expected, result)
  • EpochMillis.now() というラッパーメソッドを経由することで、現在のエポックミリ秒の取得(System.currentTimeMillis()相当)をモック化できました。

注意点2: Object、コンストラクタ、Static メソッドのモック化について

mockkObject()mockkConstructor()mockkStatic() などのモック化メソッドは、他のテストの実行時にもモック状態を維持します(バージョン 1.13.7 現在)。
他のテストケースやテストクラスの実行時にこれらのモックの影響を与えないようにするには、JUnitの事後処理メソッド(@AfterEach@AfterAll)でモック解除メソッド unmockkAll() を呼び出しておきます。

    @AfterEach
    fun tearDown() {
        unmockkAll() // テスト後、object、constructor、static のモックを解除する
    }

まとめ

MockKの基本的な使い方や、実践的な使い方のいくつかをご紹介しました。

虎の穴ラボでKotlinを使用しているプロジェクトでも実際にMockKを使ってユニットテストを書いていますが、簡単かつ柔軟にモックを扱うことができて大変重宝しています。

Kotlinプロジェクトでテストを書く際には、MockKはぜひオススメしたいライブラリです!

採用情報

虎の穴ラボでは一緒に働く仲間を募集中です!
この記事を読んで、興味を持っていただけた方はぜひ弊社の採用情報をご覧ください。
Kotlin / Next.js / TypeScript /Tailwind CSS 等、モダン環境でのシステム開発に携わることができます。
yumenosora.co.jp