LoginSignup
7
7

More than 1 year has passed since last update.

【Android】Mockito(Mockito-Kotlin) の基本的な使い方

Last updated at Posted at 2021-07-18

はじめに

自分用の備忘録として Mockito-Kotlin を使ったテストダブル(スタブ、モック、スパイ)の基本的な使い方をメモ。

ライブラリの追加

Kotlin のクラスやメソッドは、明示しない限り継承やオーバーライドが禁止されています。この状態でテストを行うとエラーが発生するため、mockito-inline を使ってエラーを回避します。

build.gradle
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 を生成するため

これらを各テストクラスに記述するのはめんどうなので、テストの基底クラスを作ると便利です。

SampleBaseTestCase.kt
open class SampleBaseTestCase {

    private lateinit var closeable: AutoCloseable

    @Before
    fun openMocks() {
        closeable = MockitoAnnotations.openMocks(this)
    }

    @After
    fun releaseMocks() {
        closeable.close()
    }
}
SampleRepositoryTest.kt
class SampleRepositoryTest: SampleBaseTestCase() {

    @Mock
    lateinit var dataSource: RemoteDataSource

    lateinit var repository: BookRepository

    @Before
    fun setUp() {
        // SampleBaseTestCase を継承しているので
        // dataSource は勝手に初期化される
        repository = SampleRepository(dataSource)
    }

    @Test
    fun sampleTest() {
        ...
    }
}

スタブ

Mockito にはスタブ専用のテストダブルが用意されていません。対象クラスをモック化してスタブメソッドを用意することでスタブを実現します。

SampleTest.kt
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 クラスのテストを考えます。

UserRepository.kt
class UserRepository(private val webservice: Webservice) {
    fun getUser(id: Int): User {
        return webservice.getUser(id)
    }

    fun saveUser(user: User) {
        webservice.saveUser(user)
    }
}
Webservice.kt
class Webservice {
    fun getUser(id: Int): User {
        return User("sample name")
    }

    fun saveUser(user: User) {
        // 保存処理
        return
    }
}

上で説明した SampleBaseTestCase クラスを継承すれば @Mock アノテーションを付けるだけで簡単にモックが作れます。また、userRepository@InjectMocks アノテーションを付けることで、インスタンス作成と @Mock が付いたモックを使って依存性注入を行ってくれます。

UserRepositoryTest.kt
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 オブジェクトの追加や取得ができるクラスをスパイ化することを考えます。

UserRepositoryTest.kt
class UserStore {

    private var users: MutableList<User> = mutableListOf()

    fun setUser(user: User) {
        users.add(user)
    }

    fun getFirstUser(): User {
        return users.first()
    }
}

コメントアウトされている whenever()... のようにスタブメソッドを定義すると エラーが発生します。

SampleSpyTest.kt
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() も使えます。

7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7