【Android】Jetpack DataStoreの単体テストをサクッと行ってみた

# はじめに
皆さん、ごきげんよう！れぶです！

今回の記事では、SharedPreferencesの代替策である`Jetpack DataStore`に関して、基本的な単体テストの手順を整理します。

**Preference DataStore（以降、DataStore）を用いた値の読み書きを単体テストレベルで検証したい方**に特に参考になればと思います。それでは、参りましょう！！

# 開発環境
* MacBook Air
* Android Studio Dolphin | 2021.3.1
* Kotlin
* compileSdkVersion 33
* targetSdkVersion 33
* minSdkVersion 21
* Jetpack DataStore 1.1.0-alpha01

# 前提
Boolean値を読み書きするDataStoreを対象とします。``saveLaunch()``の呼び出し前後で、保存された値が正しく変更できているかをテストします。

```LaunchDataStore.kt
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = "preferences"
)

//DataStoreの設定
class LaunchDataStore(private val dataStore: DataStore<Preferences>) {
    suspend fun saveLaunch() {
        this.dataStore.edit {
            it[PreferenceKeys.LAUNCH] = true
        }
    }
    val preferenceFlow: Flow<Boolean> =
        dataStore.data
            .catch { exception ->
                if (exception is IOException) {
                    emit(emptyPreferences())
                } else {
                    throw exception
                }
            }.map {
                it[PreferenceKeys.LAUNCH] ?: false
            }
}

private object PreferenceKeys {
    val LAUNCH = booleanPreferencesKey("isLaunch")
}
```

# テスト手順
### 1. 依存関係の追加
```build.gradle
    //インストゥルメント化単体テスト用
    androidTestImplementation 'androidx.test:runner:1.1.0'
    androidTestImplementation 'androidx.test:rules:1.1.0'
    //コルーチンの単体テスト用
    androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4"
```
適宜最新バージョンに合わせてください。

### 2. テストクラスの作成
今回はエミュレータで実行する単体テストであるため、``src/androidTest/java``内にテストクラスを作成します。以降の手順に関しては、このクラスに記述します。

``src/test/java``内で作成したクラスでテストしても上手く実行できないため、ご注意ください。

### 3. DataStoreインスタンスの生成
まず``PreferenceDataStoreFactory.create()``でDataStoreのインスタンスを生成します。その際に必要な引数は、事前に定義しておきます。

今回はLaunchDataStoreクラス内でDataStoreの読み書きを記述しているため、``LaunchDataStore``インスタンスも生成します。

```DataStoreUnitInstrumentedTest.kt
    private val TEST_DATASTORE_NAME = "preferences"
    private val dispatcher = UnconfinedTestDispatcher()
    private val context: Context = 
        InstrumentationRegistry.getInstrumentation().targetContext
    private val scope = TestScope(dispatcher + Job())
    private val dataStore: DataStore<Preferences> =
        PreferenceDataStoreFactory.create(
            scope = scope,
            produceFile = { context.preferencesDataStoreFile(TEST_DATASTORE_NAME) }
        )
    private val launchDataStore = LaunchDataStore(dataStore)
```

### 4. テスト前処理の設定
DataStoreの読み書きはコルーチンを元に非同期で処理されるため、テスト前に``Dispatchers.setMain``を用いて``MainDispatcher``を``TestDispatcher``に置き換える必要があります。

また、念のためDataStoreで保存されるデータをクリアにするため、DataStoreファイル全体を削除します。複数回テストを繰り返した場合に、``There are multiple DataStores active for the same file.``と怒られないように対策しています。

```DataStoreUnitInstrumentedTest.kt
    @Before
    fun setup() {
        //データを毎回新規にするためにファイル全削除
        context.run {
            File(filesDir, "datastore").deleteRecursively()
        }
        Dispatchers.setMain(dispatcher)
    }
```
### 5. テスト後処理の設定
テスト終了後は、DispatcherとTestScopeをきちんとリセットしてあげましょう。
```DataStoreUnitInstrumentedTest.kt
    @After
    fun tearDown() {
        Dispatchers.resetMain()
        scope.cancel()
    }
```
### 6. 書き込み前後のデータをテスト

さていよいよテスト本番です。非同期処理のため、``runTest``で囲みます。

デフォルトでは``false``が返ってきて、``launchDataStore.saveLaunch()``の実行後は``true``が返ってくればテスト大成功です。

```DataStoreUnitInstrumentedTest.kt
    @Test
    fun validateIsTrue() {
        scope.runTest {
            assertEquals(false,launchDataStore.preferenceFlow.firstOrNull())
            //trueに変更
            launchDataStore.saveLaunch()
            assertEquals(true,launchDataStore.preferenceFlow.firstOrNull())
        }
    }
```
## 全コード
https://github.com/seiyaleb/CoroutineTrialApp/blob/master/app/src/androidTest/java/com/coroutinetrialapp/DataStoreUnitInstrumentedTest.kt

# おわりに
今回はJetpack Preference DataStoreの単体テスト方法をサクッとまとめました。**DataStoreはコルーチンを使用している**ため、コルーチンを用いたテスト方法（「参考サイト」に記載）が理解できていれば簡単に行えるのかなと実際やってみて感じました。

上記のやり方を応用してより複雑なDataStoreの単体テストも実現できると思うので、この記事が少しでも役立つと嬉しいです。以上です。ありがとうございました！

# 参考サイト
* [DataStore and testing](https://medium.com/androiddevelopers/datastore-and-testing-edf7ae8df3d8)
* [DataStoreのユニットテスト](https://saito-yusuke.com/datastore%E3%81%AE%E3%83%A6%E3%83%8B%E3%83%83%E3%83%88%E3%83%86%E3%82%B9%E3%83%88/)
* [There are multiple DataStores active for the same file in HiltAndroidTest](https://stackoverflow.com/questions/70847060/there-are-multiple-datastores-active-for-the-same-file-in-hiltandroidtest)
* [Build instrumented tests](https://developer.android.com/training/testing/instrumented-tests)
* [Testing Kotlin coroutines on Android](https://developer.android.com/kotlin/coroutines/test)
* [DroidKaigi 2022 - Androidのモダンな技術選択にあわせて自動テストもアップデートしよう | Nozomi Takuma [JA]](https://www.youtube.com/watch?v=6O1wLS9V6XA&t=1368s)
* [Kotlin coroutines 1.6.0-RCでリワークされたkotlinx-coroutines-testを見てみる](https://qiita.com/myoshita/items/ddf32d1629abeaa8442c)