はじめに
自分用の備忘録として Mockito-Kotlin を使ったテストダブル(スタブ、モック、スパイ)の基本的な使い方をメモ。
ライブラリの追加
Kotlin のクラスやメソッドは、明示しない限り継承やオーバーライドが禁止されています。この状態でテストを行うとエラーが発生するため、mockito-inline を使ってエラーを回避します。
testImplementation "org.mockito.kotlin:mockito-kotlin:3.2.0"
testImplementation 'org.mockito:mockito-inline:3.11.2'
@Mock
@Spy
アノテーションによる初期化
mock()
や spy()
を使わなくても、@Mock
、@Spy
アノテーションでモックやスパイの初期化ができます。このとき、以下のような処理が必要となります。
- テスト開始前に
MockitoAnnotations.openMocks(this)
を実行する-
MockitoAnnotations.initMocks(this)
は非推奨となった
-
- テスト終了時に
close()
を実行する-
openMocks(this)
が AutoCloseable を生成するため
-
これらを各テストクラスに記述するのはめんどうなので、テストの基底クラスを作ると便利です。
open class SampleBaseTestCase {
private lateinit var closeable: AutoCloseable
@Before
fun openMocks() {
closeable = MockitoAnnotations.openMocks(this)
}
@After
fun releaseMocks() {
closeable.close()
}
}
class SampleRepositoryTest: SampleBaseTestCase() {
@Mock
lateinit var dataSource: RemoteDataSource
lateinit var repository: BookRepository
@Before
fun setUp() {
// SampleBaseTestCase を継承しているので
// dataSource は勝手に初期化される
repository = SampleRepository(dataSource)
}
@Test
fun sampleTest() {
...
}
}
スタブ
Mockito にはスタブ専用のテストダブルが用意されていません。対象クラスをモック化してスタブメソッドを用意することでスタブを実現します。
class StringFetcherTest: SampleBaseTestCase() {
@Mock
private lateinit var fetcher: StringFetcher
@Before
fun setUp() {
// StringFetcher#fetch() が呼ばれたら特定の値を返す
whenever(fetcher.fetch(any())).thenReturn("Hello, world")
// 引数によって返却する値を変える
whenever(fetcher.fetch(any())).thenAnswer { invocation ->
val author = invocation.arguments[0] as String
return@thenAnswer when (author) {
"福沢諭吉" -> "学問のすゝめ"
"夏目漱石" -> "吾輩は猫である"
"宮澤賢治" -> "雨ニモマケズ"
else -> "見つかりませんでした"
}
}
// 引数が空文字のとき例外を投げる
whenever(fetcher.fetch(eq(""))).thenThrow(
IllegalArgumentException("ERROR")
)
}
}
モック
developers サイトのアプリアーキテクチャ ガイドに載っている UserRepository クラスを少しアレンジしました。この UserRepository クラスのテストを考えます。
class UserRepository(private val webservice: Webservice) {
fun getUser(id: Int): User {
return webservice.getUser(id)
}
fun saveUser(user: User) {
webservice.saveUser(user)
}
}
class Webservice {
fun getUser(id: Int): User {
return User("sample name")
}
fun saveUser(user: User) {
// 保存処理
return
}
}
上で説明した SampleBaseTestCase
クラスを継承すれば @Mock
アノテーションを付けるだけで簡単にモックが作れます。また、userRepository
に @InjectMocks
アノテーションを付けることで、インスタンス作成と @Mock
が付いたモックを使って依存性注入を行ってくれます。
class UserRepositoryTest: SampleBaseTestCase() {
@Mock
lateinit var webservice: Webservice
@InjectMocks
lateinit var userRepository: UserRepository
@Test
fun sampleTest() {
val user = User(name = "伊藤")
userRepository.saveUser(user)
// Webservice#saveUser(User) が1回呼ばれたことを確認
verify(webservice, times(1)).saveUser(user)
// Webservice#saveUser(User) が少なくとも1回呼ばれたことを確認
verify(webservice, atLeast(1)).saveUser(user)
// Webservice#saveUser(User) が最大5回まで呼ばれたことを確認
verify(webservice, atMost(5)).saveUser(user)
// Webservice#getUser(User) が呼ばれていないことを確認
verify(webservice, never()).getUser(any())
// Webservice#saveUser(User) の引数を検証
argumentCaptor<User> {
verify(webservice, times(1)).saveUser(capture())
assertEquals(firstValue.name, eq("伊藤"))
}
}
}
スパイ
スパイについても @Spy
アノテーションを付けることで簡単に作ることができます。使い方はスタブとほぼ同じですが、スタブメソッドを定義するときに少し注意が必要です。
以下、適当に作ったクラスですが User オブジェクトの追加や取得ができるクラスをスパイ化することを考えます。
class UserStore {
private var users: MutableList<User> = mutableListOf()
fun setUser(user: User) {
users.add(user)
}
fun getFirstUser(): User {
return users.first()
}
}
コメントアウトされている whenever()...
のようにスタブメソッドを定義すると エラーが発生します。
class SampleSpyTest: SampleBaseTestCase() {
@Spy
lateinit var store :UserStore
@Test
fun spyTest() {
// これだとエラーになる
// whenever(store.getFirstUser()).thenReturn(User("伊藤"))
// これだと大丈夫
doReturn(User("伊藤")).whenever(store).getFirstUser()
val user = store.getFirstUser()
assertEquals(user.name, "伊藤")
}
}
スパイはスタブと同じようにテスト対象に対して任意の値を提供することができます。ただし、スパイは実際のインスタンスを利用する(実インスタンスと呼び出し元の間でプロキシ的な働きをする)ため store.getFirstUser()
のとき、実際に UserStore のプロパティ users
へアクセスしします。呼び出した時点で users
が空配列であればエラーが発生してしまいます。
これを解消するために、Mockito には前置記法が用意されています。
doReturn(User("伊藤")).whenever(store).getFirstUser()
こうすることで、実インスタンスのメソッドが実行される前に任意の値を返すことができるようになります。doReturn()
以外にも doAnswer {...}
や doThrow()
も使えます。