Kotlin向けMockライブラリのMockKの使い方について簡単にまとめます
MockKがどんな意図で作られているかはこちらの記事で作者が語られていますのでご参照ください。
MockKとは
- MockKはKotlin独自の言語仕様をほぼ網羅しているモックライブラリ
-
Coroutine
やobject
、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つずつ説明すると
-
val car = mockk<Car>()
- モックインスタンスを生成する
-
every { car.drive(Direction.NORTH) } returns Outcome.OK
- モックインスタンスにパターンを設定
- この場合は「carインスタンスのdriveメソッドが引数Direction.NORTHで呼ばれた場合はOutcome.OKを返却する」となる
-
car.drive(Direction.NORTH) // returns OK
- 実際に呼び出す(Outcome.OKが返却される)
-
verify { car.drive(Direction.NORTH) }
- メソッド呼び出しのチェック
- この場合は「carインスタンスのdriveメソッドが引数Direction.NORTHで呼び出されている」という意味
-
verify(exactly = 1) { car.drive(Direction.NORTH) }
というようにexactly
で呼び出されている回数を指定してチェックすることもできる
-
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 outcome
がOutcome(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()
を利用します。
また、unmockkAll
かunmockkObject
を使用することでモックの解除もできます。
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()
}
}
}
また、スコープがモジュール(トップレベル宣言)の場合は以下のように実装します
package com.hoge.sample
data class Obj(val value: Int)
fun Obj.extensionFunc() = value + 5
@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
の部分の命名を変えることもできます。
@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にはまだまだたくさんの機能があります。
全部を紹介するとかなりのボリュームになってしまうので、自分がよく使うものを紹介させていただきました。
このライブラリだけで、ここまで機能がそろっていると安心してテストがかけますね!