Posted at

MockKの使い方

Kotlin向けMockライブラリのMockKの使い方について簡単にまとめます

MockKがどんな意図で作られているかはこちらの記事で作者が語られていますのでご参照ください。


MockKとは


  • MockKはKotlin独自の言語仕様をほぼ網羅しているモックライブラリ


  • Coroutineobject、private関数などにも対応している優れもの


    • 今回はCoroutineには触れませんので、あしからず。。。



  • 公式ドキュメントにすべて書かれていますので、詳細はこちらを見てください



今回は公式ドキュメントの内容からいくつか代表的なもの(自分がよく使うもの)を紹介させていただきます。

内容に不備等ありましたら、やさしくマサカリ投げてくださいm(_ _)m


動作確認環境について

MockKはKotlin全般で利用できるライブラリですが、今回はAndroid Studioを利用しています。

特にAndroid SDKには依存していないユニットテストで書いていますので、他の環境でも動作するとは思います。


  • MockK 1.9

  • Android Studio 3.3.1

  • junit 4.12


MockKの使い方


セットアップ

下記を参考に、ご自身の環境に合わせてインストールしてください。


DSL

DSLとは domain-specific language(ドメイン固有言語)のことで、特定の領域に特化した言語のことです。

書き方は以下のようになります。(公式ドキュメントより)

val car = mockk<Car>()

every { car.drive(Direction.NORTH) } returns Outcome.OK
car.drive(Direction.NORTH) // returns OK
verify { car.drive(Direction.NORTH) }
confirmVerified(car)

これは公式ドキュメントに記載されている通りですが、1つずつ説明すると



  1. val car = mockk<Car>()


    • モックインスタンスを生成する




  2. every { car.drive(Direction.NORTH) } returns Outcome.OK


    • モックインスタンスにパターンを設定

    • この場合は「carインスタンスのdriveメソッドが引数Direction.NORTHで呼ばれた場合はOutcome.OKを返却する」となる




  3. car.drive(Direction.NORTH) // returns OK


    • 実際に呼び出す(Outcome.OKが返却される)




  4. verify { car.drive(Direction.NORTH) }


    • メソッド呼び出しのチェック

    • この場合は「carインスタンスのdriveメソッドが引数Direction.NORTHで呼び出されている」という意味


    • verify(exactly = 1) { car.drive(Direction.NORTH) }というようにexactlyで呼び出されている回数を指定してチェックすることもできる




  5. confirmVerified(car)



    • verifyで設定した全ての検証が完了していることをチェックする

    • 呼び出されていないものがあると例外をスローする

    • 例えば、以下の場合は例外がスローされる(car.drive(Direction.NORTH)をチェックしていないため)



val car = mockk<Car>()

every { car.drive(Direction.NORTH) } returns Outcome.OK
every { car.drive(Direction.SOUTH) } returns Outcome.OK

car.drive(Direction.NORTH) // returns OK
car.drive(Direction.SOUTH) // returns OK

verify {
car.drive(Direction.SOUTH)
}

confirmVerified(car)


Annotations

MockKでは便利なDI用のアノテーションがあります。

それらを使って宣言することでモックインスタンスの生成が楽になります。

MockKAnnotations.init()を呼び出すことでインジェクションされます。

アノテーションの種類



  • @MockK


    • モックインスタンスとしてインジェクションしたい場合に使用します




  • @RelaxedMockK


    • Relaxedモックインスタンスとしてインジェクションしたい場合に使用します

    • Relaxedモックインスタンスは後述します




  • @SpyK


    • Spy用のインスタンスとしてインジェクションしたい場合に使用します

    • Spyについては後述します。




  • @InjectMockKs


    • 該当オブジェクトのもつ属性に対してインジェクトしたい場合に使用します

    • 具体的には以下のような場合です



class TrafficSystem {

lateinit var car1: Car
lateinit var car2: Car
lateinit var car3: Car
}

@InjectMockKs
var trafficSystem = TrafficSystem()

// trafficSystemの属性car1, car2, car3にモックインスタンスがインジェクションされる
@Before
fun setUp() = MockKAnnotations.init(this)


Spy

Spyはオブジェクトを実際のコードで動かしつつ、メソッドの引数や呼び出し回数、戻り値などを検証するために使用します

val car = spyk<Car>()

car.drive(Direction.NORTH)
verify {
car.drive(Direction.NORTH)
}
confirmVerified(car)

注意点としては、アノテーションを利用して初期化する際には@Spyk var car = Car()のようにvarかつインスタンス生成する必要があります。

以下のようにすると初期化時に例外がスローされるので注意しましょう。

@Spyk

lateinit var car: Car

// 初期化時に例外がスローされる
@Before
fun setUp() = MockKAnnotations.init(this)

@Test
fun test() {
car.drive(Direction.NORTH)
}


Relaxed mock

ざっくり言うと、ダミーの値を返すモックを自動で作ってくれます。

下記の場合だと、driveの戻り値のval outcomeOutcome(name = null, ordinal = 0)というような形のダミー値が返却されるようになります。

戻り値の検証を必要としないテストの場合は、モックを実装する手間が省けるので便利です。

enum class Outcome { OK, NG }

enum class Direction { NORTH, SOURTH, WEST, EAST }

interface Car {
fun drive(direction: Direction): Outcome
}

fun testMockKSample() {
val car = mockk<Car>(relaxed = true)
val outcome = car.drive(Direction.NORTH)
verify { car.drive(Direction.NORTH) }
confirmVerified(car)
}

注意点としては、ジェネリクスを使った場合はClassCastExceptionが発生してしまいます。

// NG

interface Factory {
fun <T> create(): T
}

@Test
fun testMockKSample() {
val factory = mockk<Factory>(relaxed = true)
val car = factory.create<Car>() // throw ClassCastException
}

この場合は、everyを利用してモックオブジェクトの実装をしてあげましょう


  • every { factory.create<Car>() } returns Car()


Mock relaxed for functions returning Unit

戻り値がUnitのものだけRelaxed mockしてくれる機能です。

利用方法は以下の3種類です。


  • 関数


    • mockk<Car>(relaxUnitFun = true)



  • Annotations


    • @MockK(relaxUnitFun = true)



  • MockKAnnotations.init


    • MockKAnnotations.init(this, relaxUnitFun = true)




Object Mock

MockKではobjectもモックできます。

モック化にはmockkObject()を利用します。

また、unmockkAllunmockkObjectを使用することでモックの解除もできます。

object MockObj {

fun add(a: Int, b: Int) = a + b
}
@Before
fun setUp() {
mockkObject(MockObj) // これでモックになる
}

@Test
fun testMockKSample() {
assertEquals(3, MockObj.add(1, 2))
every { MockObj.add(1, 2) } returns 55
assertEquals(55, MockObj.add(1, 2))
}

@After
fun tearDown() {
unmockkAll() // or unmockkObject(MockObj)
}


Extension functions

拡張関数は3種類のスコープで実装できます


  • class

  • object

  • module

classとobjectの場合はmockkを利用してモック化が可能です。

data class Obj(val value: Int)

class Ext {
fun Obj.extensionFunc() = value + 5
}

@Test
fun testMockKSample() {
with(mockk<Ext>()) {
every {
Obj(5).extensionFunc()
} returns 11

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

verify {
Obj(5).extensionFunc()
}
}
}

また、スコープがモジュール(トップレベル宣言)の場合は以下のように実装します


app/src/main/kotlin/com/hoge/sample/File.kt

package com.hoge.sample

data class Obj(val value: Int)
fun Obj.extensionFunc() = value + 5



app/src/test/kotlin/com/hoge/sample/FileTest.kt

@Test

fun testMockKSample() {
mockkStatic("com.hoge.sample.FileKt")

every {
Obj(5).extensionFunc()
} returns 11

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

verify {
Obj(5).extensionFunc()
}
}


@JvmNameを利用して上記のFileKtの部分の命名を変えることもできます。


app/src/main/kotlin/com/hoge/sample/File.kt

@file:JvmName("KFile")

package com.hoge.sample

data class Obj(val value: Int)
fun Obj.extensionFunc() = value + 5


実際に色々な拡張関数をモック化しようとすると、予測できないクラス名があったりします。

例えばFile.endsWith()だとmockkStatic("kotlin.io.FilesKt__UtilsKt")のようにする必要があります。

これを調べるにはKotlinのコードを一度バイトコードにしてからJavaのclassファイルを調べるとわかります。

Android Studioの場合は「Tools -> Kotlin -> Show Kotlin Bytecode」を選択後、Javaにデコンパイルするとわかります。


Private functions mocking

privateな関数をモックするには、モックインスタンスを生成する際にrecordPrivateCallsの引数にtrueを渡すと実装することができます。

具体的には以下の例を参照してください。

今回はdrive()の処理はそのまま動かしたいのでspykを利用しています。

class Car {

fun drive() = accelerate()

private fun accelerate() = "going faster"
}

@Test
fun testMockKSample() {
val mock = spyk<Car>(recordPrivateCalls = true)

every { mock["accelerate"]() } returns "going not so fast"

assertEquals("going not so fast", mock.drive())

verifySequence {
mock.drive()
mock["accelerate"]()
}
}


最後に

MockKにはまだまだたくさんの機能があります。

全部を紹介するとかなりのボリュームになってしまうので、自分がよく使うものを紹介させていただきました。

このライブラリだけで、ここまで機能がそろっていると安心してテストがかけますね!