📌

DI(Dependency Injection)したRepositoriesのUnit Testをする【Android kotlin】

2022/01/12に公開

やりたいこと

DI (Dependency Injection)したRepositoriesのUnit Testをしたい。

この記事で書くこと

「やりたいこと」をするに当たって調べたこと、試したことを、メモがてら書きます。
もしも誤りなどがあれば、コメントにて指摘いただけると幸いです。
Repositoriesという概念ではなく、DIした(された?)classに対して使えると思います。
今回のDIはDaggerのConstructor Injectionです。
具体的に書く内容は以下の4つです。

  1. DIとはそもそも何か(概念)
  2. テスト対象のRepositoriesとは何か〜Androidのアーキテクチャガイド〜
  3. テスト対象の実装コードと、その説明
  4. 動いたテストコードと、その説明

DI(Dependency Injection)とは何か

DIとは簡単に言うと、あるオブジェクトの依存関係にあるオブジェクトを外部から入れる(注入する)という仕組みのことをいいます[1]
例えば、Aの中で依存関係にあるインスタンスをBというオブジェクトに切り離して、BをAに入れる仕組みということです。
この仕組を使うことで、例えばテストしたいオブジェクトの依存関係にあるオブジェクトが完成していなくても、Mockで代用できたりします。DIすると何が嬉しいかなどは、@hshimoさんの記事[2]が分かりやすかったように思います。
今回は、DIじゃなかったら、なぜ駄目なのかという実験はしないのですが、今後、時間がある時に体験してみたいと思います。

今回テスト対象のRepositoriesとは何か

これは、なぜDIが必要なのかという説明になります。
現在、私が開発しているアプリは、Androidのアーキテクチャガイド[3]に則って開発をしています。

App Architecture

この図について少し説明します。RepositoriesはViewには依存していないので省きます。一番下のData Sourcesは、端末内のファイルを読んだり、サーバーからデータを取ってきたり、端末内のデータベースを操作したり、それぞれのデータソースのみを処理する役割を担います。次にRepositoriesですが、これはData Layerのエントリーポイントになります。したがって、Data Layerにアクセスする際は必ずRepositoriesを通ることとなります。Repositoriesでは、ビジネスロジックを持ったり、Data Sourcesを抽象化したりします。ユーザーの入力をData Sourcesに処理させるために加工したり、何かしらの処理をしてUI Layerに返したり、処理の一連の流れを保持したりします。
したがって、RepositoriesはData Sourcesと依存関係にある[4]事がわかります。
前章の追加説明になりますが、DIを使うことでData Sourcesが出来ていなくてもRepositoriesをテストできるということです。一方で、たとえData Sourcesが出来ていてもRepositoriesの機能を検証するには、同じくDIをして分離してテストしたほうが良いように見えます。

テスト対象の実装コードと、その説明

今回は、ユーザーのログイン情報を処理するData Sourceからデータを貰い、それを加工してログインユーザーのメールアドレスと表示名を返すRepositoryを作成したいと思います。今回はニュース記事を購読するアプリを想定し、AuthInfoにはメールアドレスと表示名の他に支払いの有無や購読しているIssueを持っていることとします。

UserDataSource.kt
//ユーザーのログイン情報を処理するオブジェクトのInterface
interface UserDataSource {
    suspend fun getAccountInfo(): AuthInfo
}

//ユーザーのログイン情報
data class AuthInfo(
    val displayedName: String,
    val email: String,
    val payment: Boolean,
    val subscribedIssues: List<String>,
)

では、Repositoryの実装です。

UserRepository.kt
interface UserRepository {
    suspend fun getUserInfo(): InfoUser
}

data class InfoUser(
    val displayedName: String,
    val email: String,
)
UserRepositoryImpl.kt
class UserRepositoryImpl @Inject constructor(
    val userDataSource: UserDataSource,
): UserRepository {
    override suspend fun getUserInfo(): InfoUser {
        val userInfo = userDataSource.getAccountInfo()
        return InfoUser(
            displayedName = userInfo.displayedName,
            emailAddress = userInfo.email
        )
    }
}
UserRepositoryModule.kt
@Module
@InstallIn(ViewModelComponent::class)
class UserRepositoryModule {
    @Provides
    fun provideUserRepository(userRepositoryImpl: UserRepositoryImpl): UserRepository {
        return userRepositoryImpl
    }
}

このように、UserDataSourceは未実装です。MockのUserDataSourceがAuthInfoを返してくれれば、UserRepositoryをテストできます。

動いたテストコードと、その説明

今回、参考にしたのは@toshihirooyaさんの記事[5]です。ただ、躓いた点があるので、そこを共有したいと思います。

躓いたところ

まず、mockの作成にMockitoを使うのですが、dependenciesにはmockito-coreではなくmockito-inlineを入れます。これは、kotlinではデフォルトがfinal classで、このmockをmockito-coreでは作れないからです。
2つ目はバージョンについてですが、ネットに落ちているサンプルは多くがversion2なのですが、最新バージョンである4.2.0を使ったほうが良いです。他のバージョンではJava11で動かないことがあります。以上のことから、app/build.gradleに以下のコードを入れてください。

app/build.gradle
dependencies {
...中略
    // Mockito framework
    testImplementation 'org.mockito:mockito-inline:4.2.0'
}

この点がクリアできれば、後は@toshihirooyaさんの記事[5:1]とほぼ同様です。

テストコード

一応コードを貼っておきます。

UserRepositoryImplTest.kt
@RunWith(MockitoJUnitRunner::class)
class UserRepositoryImplTest {
    
    @Test
    fun getPrimaryAccountInfoTest() {
        runBlocking {
            val dummyData = AuthInfo(
		    displayedName = "test",
		    email = "test@test.test",
		    payment = true,
		    subscribedIssues = listOf("no.1", "no.2"),
		)
            val mockUserDataSource = mock(UserDataSource::class.java).apply {
                `when`(getAccountInfo()).thenReturn(dummyData)
            }

            val userRepositoryImpl: UserRepositoryImpl = UserRepositoryImpl(mockUserDataSource)
            val result: InfoUser = userRepositoryImpl.getUserInfo()

            assertEquals(result, InfoUser(
                displayedName = dummyData.displayedName,
                emailAddress = dummyData.email,
                ))
        }
    }
}

最後に

自分のアプリで実装しているコードを、記事上で書き換えたので、動かない場合はコメントにて指摘いただけると幸いです。

脚注
  1. DIの基本とAndroidでのDI利用パターンについて / @matsurih on Qiita https://qiita.com/matsurih/items/fed6daceeb48f64d4ef5 (2022-01-12閲覧) ↩︎

  2. 猿でも分かる! Dependency Injection: 依存性の注入 / @hshimo on Qiita https://qiita.com/hshimo/items/1136087e1c6e5c5b0d9f (2022-01-12閲覧) ↩︎

  3. アプリ アーキテクチャ ガイド / Android https://developer.android.com/jetpack/guide?hl=ja (2022-01-12閲覧) ↩︎

  4. データレイヤ / Android https://developer.android.com/jetpack/guide/data-layer?hl=ja ↩︎

  5. Kotlin、MockitoによるRepositoryパターンのLocal Unit Testについて / @toshihirooya on Qiita https://qiita.com/toshihirooya/items/4f051881ec0f103b52ce ↩︎ ↩︎

Discussion