【Flutter】具体例で学ぶ shared_preferences と Riverpod を使ったユニットテスト

## はじめに

私の現場ではRiverpodを使った状態管理とDIで、レイヤードアーキテクチャ（またはリバーポッドアーキテクチャ）を採用しています。

https://codewithandrea.com/articles/flutter-app-architecture-riverpod-introduction/

メンバーにタスクが振られ、実装が完了したらテストコードも合わせて書いてプルリクエストを提出しています。

私は実務未経験で今回の現場に配属されましたが、そんな実務の経験を経てテストコードを書く技術も蓄積することができました。

この記事ではその中から、ユニットテスト(unit test)に焦点を当ててお話ししていきます。
題材としてはdata層に作成したshared_prefernsesのrepositoryをテストしていきます。

https://pub.dev/packages/shared_preferences

shared_prefernsesはローカルデータベースにキーとバリューの組み合わせでデータを保存する仕組みを提供するパッケージです。

### 記事の対象者

- Riverpodを交えたテストコードを学びたい方
- レイヤードアーキテクチャなど抽象化さて書かれたコードのテストについて学びたい方
- shared_prefernsesのテストについて学びたい方

### 記事を執筆時点での筆者の環境

```
[✓] Flutter (Channel stable, 3.22.1, on macOS 14.3.1 23D60 darwin-arm64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 15.2)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2023.3)
[✓] VS Code (version 1.90.1)
```

### サンプルプロジェクト

shared_preferencesを使ってローカルに４つの値を保存するアプリです。
- アイコンの設定：`bool`
- 背景色の番号：`int`
- タイトルの文字：`String`
- JSONで複数の設定：`CustomSetting`/文字列からJSONに変換

また、赤文字のボタンをタップしてデータを初期化（リセット）できます。

![【Flutter】riverpodを使ったDIとレイヤードアーキテクチャ、特にデータ層について（仮）.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058346/6b245cb7-0bbe-0829-2702-f9be3289fa71.gif)

### ソースコード

https://github.com/HaruhikoMotokawa/shared_preference_sample

### 実装内容について

今回は実装内容についての紹介ではなく、あくまでテストに関する内容のため詳細の説明は割愛します。
適時必要な部分は抜粋していきますが、不足する部分があるかもしれません。
ソースコードで確認していただくか、以下の記事でご確認ください。

https://qiita.com/Harx02663971/items/e04b059f5148c34f7a60

https://qiita.com/Harx02663971/items/1e9d8efc68c88385ac7a



### ユニットテストの基礎について

今回はユニットテストの基本的な関数やテストの実行方法を理解している前提でお話しします。
もしもその部分がまだ不安な方は以前書いた記事がありますので、ぜひそちらをご覧ください。

https://qiita.com/Harx02663971/items/1ff5a9d9b79a7e1d663c

## 1. フォルダとファイルの作成

こちらは[【Flutter】ユニットテスト基本のき　2-1. フォルダとファイルの作成](https://qiita.com/Harx02663971/items/1ff5a9d9b79a7e1d663c#2-%E3%83%A6%E3%83%8B%E3%83%83%E3%83%88%E3%83%86%E3%82%B9%E3%83%88%E3%81%AE%E6%BA%96%E5%82%99)の転載になります。

テストのファイルはtestフォルダー内に作成します。
原則はテスト対象のファイルが置かれているディレクトリ構成に則ってディレクトリとテストファイルを作成します。
そのほうが探す際にわかりやすいからです。

今回で行けば以下のrepository.dartとprovider.dartのテストを書きたいです。

```

lib
├── data
│   ├── local_sources
│   │   ├── shared_preference.dart
│   │   └── shared_preference.g.dart
│   └── repositories
│       └── key_value_repository
│           ├── provider.dart // <= 💡 ここをテストしたい
│           ├── provider.g.dart
│           └── repository.dart // <= 💡 ここをテストしたい

```

その場合は以下のようにディレクトリを作り、ファイルを作成します。

```

test
├── data
│   └── repositories
│       └── key_value_repository
│           └── repository_test.dart // <= 😀

```

スクショで見るとこのような形になります。

![スクリーンショット 2024-06-22 18.13.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058346/baccb330-9daa-56e4-a33f-be4787dc6c3b.png)

:::note warn
テストファイルは**名称の末尾を必ず_test.dart**としないとテスト用のファイルだと認識されずにテストが実行できません。
ファイル作成後は必ず確認しましょう！
:::

:::note
ファイルを手動で作っていってもいいですが、私は必要なディレクトリごとコピーして、
テストファイルに貼り付けます。
あとはファイルの名前の末尾に**_test**をつけて、ファイルの中身を一度全て削除してしまえば
そっくりそのままディレクトリ構造も作れてお手軽なのでおすすめです。
:::

## 2. repository.dartをテストする

### 2-1. 実行環境の準備

まず初めにファイルの中身を準備していきます。
[【Flutter】ユニットテスト基本のき　2-2. 実行環境](https://qiita.com/Harx02663971/items/1ff5a9d9b79a7e1d663c#2-2-%E5%AE%9F%E8%A1%8C%E7%92%B0%E5%A2%83)で紹介したテンプレートをコピーして貼り付けます。
その上で今回のテストしたいグループの数、テストケースの数に合うように修正します。

エディタを分割して、左か右側に実装したコードを載せておくと内容を確認しながら書けるのでおすすめです。
今回の実装は**lib/data/repositories/key_value_repository/repository.dart**です。

![スクリーンショット 2024-06-23 21.57.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058346/9422cc4f-3f7b-7010-b657-303f7cbd7a01.png)

:::note
左がテスト、右が実装です。
テストはすでに書いてある時のスクショのため、コードブロックを折りたたんで撮影しています。
:::

以下はテストコード内容です。
```repository.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  late ProviderContainer container;

  setUp(() {
    container = ProviderContainer();
  });
  tearDown(() {
    container.dispose();
  });
  group('key_value_repository test', () {
    test('値の変更を監視することができる', () {});
    test('アイコンの設定を取得し、保存できる', () {});
    test('背景色の番号を取得し、保存できる', () {});
    test('タイトルのテキストを取得し、保存できる', () {});
    test('カスタム設定を取得し、保存できる', () {});
    test('全てのデータを初期化できる', () {});
  });
}

```
グループに関してはエラーハンドリングや初期データを分けたテストなどはしていないため、１つにまとめています。

テストしたい内容は本来であればそのタスクの仕様に合わせて設定します。
つまり必ずしもメソッド単位というわけではありません。
今回は厳密に仕様を決めていないこともあるので、`test()`のコメントにある通り保存と取得関連は対象のデータごとにそれぞれまとめ、合計６個のテストケースに分かれています。

### 2-2. 定数&変数、setUp、tearDownの設定

```repository.dart

void main() {
  /// KeyValueRepositoryBaseを使ってKeyValueRepositoryの機能にアクセスする
  late KeyValueRepositoryBase keyValueRepository;

  /// Riverpodを使用しているので、ProviderContainerを経由する
  late ProviderContainer container;

  setUp(() async {
    // 最初にmockの初期値を設定
    // これを最初に実行しないとローカルデータベースに影響が出る
    // 設定しておきたい初期値があるならここで設定する
    SharedPreferences.setMockInitialValues({
      // 例
      // KeyValueRepository.iconSettingKey : true,
    });
    container = ProviderContainer();
    keyValueRepository = container.read(keyValueRepositoryProvider);
  });
  tearDown(() {
    container.dispose();
  });

  // 省略

```

#### 定数&変数
- `late KeyValueRepositoryBase keyValueRepository;`
今回は実装クラスの抽象化を行なっているので、呼び出しはインターフェースである`KeyValueRepositoryBase`となります。

- ` late ProviderContainer container;`
依存性の注入を行うのにRiverpodを仕様しています。`KeyValueRepositoryBase`へアクセスする際に`container`を経由するために使用します。

:::note
ProviderContainerについて
実は今回でいえば`overrides`で上書きすることはないので、本来は
`final container = ProviderContainer();`で宣言しても良いです。
ただ大体の場合は`setUp()`関数内で`overrides`することが多いため
そのままにしています。
ここは慣れてきたらしっかりと使い分けてもいいかもしれません。
:::

#### `setUp`
今回設定するのは３点です。
- `SharedPreferences.setMockInitialValues({})`
主にエミュレータなどのデバイスデータに影響を与えないためテスト環境上にmockの保存先を作成します。
今回は初期値なしの状態でテストします。
- `container = ProviderContainer();`
先ほど定数で宣言した`container`を設定。今回は特にないので`ProviderContainer()`を代入するのみです。
- `keyValueRepository = container.read(keyValueRepositoryProvider);`
実装を見てみるとわかるのですが、`KeyValueRepositoryBase`は`keyValueRepositoryProvider`経由でアクセスできます。
`container`経由でインスタンスを取得しています。
そして`keyValueRepositoryProvider`が最終的に返却するのは
実際の実装を持っている`KeyValueRepository(ref);`です。
ここら辺が少し複雑ですが、頑張って整理していきましょう。

```lib/data/repositories/key_value_repository/provider.dart

/// `KeyValueRepositoryBase` のインスタンスを生成
@Riverpod(keepAlive: true)
KeyValueRepositoryBase keyValueRepository(KeyValueRepositoryRef ref) {
  return KeyValueRepository(ref);
}

```

:::note
データベースのmock化
SharedPreferencesに限らずデータベース関連のパッケージであればテスト用のmock化が容易されていると思います。
まずは公式ドキュメントを確認しましょう。
:::

#### `tearDown()`
`container.dispose();`のみです。
テスト用に作成したProviderContainerを破棄してリソースを解放します。
毎回破棄することでインスタンスの重複を防止して不要なメモリの消費を防ぎます。


これで基本の準備、設定は完了です。次からテストを書いていきます。

### 2-3. onValueChangeのテスト

以下に関連する実装部分を抜粋しています。
```lib/data/repositories/key_value_repository/repository.dart

  /// アイコン設定のキー
  static const iconSettingKey = 'iconSetting';

  /// 設定値の変更をアプリケーション全体にブロードキャストするための`StreamController`
  final _onValueChanged = StreamController<String>.broadcast();

  @override
  Stream<String> get onValueChange => _onValueChanged.stream;

  @override
  Future<void> setIconSetting({bool? value}) => _set(iconSettingKey, value);

  Future<void> _set(String key, Object? value) async {
    final pref = await ref.read(sharedPreferencesProvider.future);

    switch (value) {
      case final int intValue:
        await pref.setInt(key, intValue);
      case final double doubleValue:
        await pref.setDouble(key, doubleValue);
      case final bool boolValue:
        await pref.setBool(key, boolValue);
      case final String stringValue:
        await pref.setString(key, stringValue);
      case final DateTime dateTimeValue:
        await pref.setString(key, dateTimeValue.toIso8601String());
      case final List<String> listStringValue:
        await pref.setStringList(key, listStringValue);
      case null:
        await pref.remove(key);
      case _:
        await pref.setString(key, jsonEncode(value));
    }

    // keyという文字列をストリームに流す
    _onValueChanged.add(key);
  }

```

テストしたものが以下です。
```test/data/repositories/key_value_repository/repository_test.dart

test('値の変更を監視することができる', () async {
      // ストリームの値が流れてきたら格納する変数を定義
      final histories = <String>[];

      // onValueChangeの購読を宣言し、ストリームが流れてきたらhistoriesに追加する
      keyValueRepository.onValueChange.listen(histories.add);

      // setIconSettingメソッドを実行する
      await keyValueRepository.setIconSetting(value: true);

      // 結果が反映されるまで少し待機
      await Future<void>.delayed(Duration.zero);

      // 格納されている値は'iconSetting'である
      expect(histories, [KeyValueRepository.iconSettingKey]);
    });
    
```
テスト内で`Future`型のメソッドを扱うため、`test()`に`async`を付与しています。

`onValueChange`は`String`の値を受け取る`Stream`です。
そこで`Stream`の受け取り先を`histories`というリストの変数で受け取ります。

`onValueChange`で購読を宣言した後に`setIconSetting`メソッドを実行しています。
`setIconSetting`メソッドの内部実装は`_set()`メソッドで、その実行の最後に
`_onValueChanged.add(key);`で値をストリームに流しています。
よってここで'iconSetting'というキーが`histories`に格納されます。

すぐに`expect`で検証に入ってしまうと、`setIconSetting`メソッドの実行は終わっているのですが、ストリームに値がまだ流れておらず結果が`[ ]`となって失敗してしまいます。
そこで`await Future<void>.delayed(Duration.zero);`で少し待機する必要があります。

最後に`expect()`で値が流れてきているかの検証を行なって完了です。

### 2-4. 取得と保存のテスト
今回は４種類のデータの保存と取得メソッドがありますが、どれもほとんど同じですのでここでは「アイコンの設定を取得し、保存できる」、のテストについて解説します。

以下、実装の抜粋です。

```lib/data/repositories/key_value_repository/repository.dart

  @override
  Future<bool?> getIconSetting() => _get(iconSettingKey);

  @override
  Future<void> setIconSetting({bool? value}) => _set(iconSettingKey, value);

```

以下がテスト内容です。

```test/data/repositories/key_value_repository/repository_test.dart

    test('アイコンの設定を取得し、保存できる', () async {
      // アイコンの設定を取得
      var iconSetting = await keyValueRepository.getIconSetting();

      // 初期値はnullである
      expect(iconSetting, isNull);

      // アイコンの設定をtrueで保存
      await keyValueRepository.setIconSetting(value: true);

      // アイコンの設定を取得
      iconSetting = await keyValueRepository.getIconSetting();

      // アイコンの設定はtrueである
      expect(iconSetting, true);
    });
    
```

`onValueChange`の時と同様に`test()`に`async`を付与しています。

まずは変数に`getIconSetting()`メソッドを実行して値を取得します。

初期値がnullであることを確認します。
今回の実装では最初に初期値がなかった場合は初期値を設定するのではなくnullのままにしています。

次にアイコンの設定を保存します。
アイコンの設定を再び取得して変わっていることを確認します。

このことからアイコンの設定を取得し、また保存できることを確認することができました。

### 2-5. データの初期化のテスト
いかが実装部分です。

```lib/data/repositories/key_value_repository/repository.dart

  @override
  Future<void> initData() async {
    final pref = await ref.read(sharedPreferencesProvider.future);
    await pref.clear();

    // カスケード記法で重複する`_onValueChanged`を一つに省略している
    _onValueChanged
      ..add(iconSettingKey)
      ..add(backgroundColorNumberKey)
      ..add(titleTextKey)
      ..add(customSettingKey);
  }

```

以下がテスト内容です。
```test/data/repositories/key_value_repository/repository_test.dart

    test('全てのデータを初期化できる', () async {
      // アイコンの設定をtrueで保存
      await keyValueRepository.setIconSetting(value: true);

      // 背景色の番号を1で保存
      await keyValueRepository.setBackgroundColorNumber(1);

      // タイトルのテキストを'iOS'で保存
      await keyValueRepository.setTitleText('iOS');

      // 保存するカスタム設定を定義
      const saveCustomSetting = CustomSetting(
        iconSetting: true,
        backgroundColorNumber: 1,
        titleText: 'iOS',
      );

      // カスタム設定を保存
      await keyValueRepository.setCustomSetting(saveCustomSetting);

      // アイコンの設定を取得
      var iconSetting = await keyValueRepository.getIconSetting();

      // 背景色の番号を取得
      var backgroundColorNumber =
          await keyValueRepository.getBackgroundColorNumber();

      // タイトルのテキストを取得
      var tileText = await keyValueRepository.getTileText();

      // カスタム設定を取得
      var customSetting = await keyValueRepository.getCustomSetting();

      // アイコンの設定はtrueである
      expect(iconSetting, true);
      // 背景色の番号は1である
      expect(backgroundColorNumber, 1);
      // タイトルのテキストは'iOS'である
      expect(tileText, 'iOS');
      // カスタム設定はsaveCustomSettingと同じである
      expect(customSetting, saveCustomSetting);

      // 全てのデータを初期化
      await keyValueRepository.initData();

      // アイコンの設定を取得
      iconSetting = await keyValueRepository.getIconSetting();

      // 背景色の番号を取得
      backgroundColorNumber =
          await keyValueRepository.getBackgroundColorNumber();

      // タイトルのテキストを取得
      tileText = await keyValueRepository.getTileText();

      // カスタム設定を取得
      customSetting = await keyValueRepository.getCustomSetting();

      // アイコンの設定はnullである
      expect(iconSetting, isNull);
      // 背景色の番号はnullである
      expect(backgroundColorNumber, isNull);
      // タイトルのテキストはnullである
      expect(tileText, isNull);
      // カスタム設定はnullと同じである
      expect(customSetting, isNull);
    });
    
```

内容は今まで一番長いですが、実は保存と取得する項目が増えただけです。
まず最初に保存系４つのメソッド（`setXxxx()`）を実行して値を保存し、
保存されていることを確認します。
その後、`await keyValueRepository.initData();`を実行して、データをすべて消します。
最後にすべての保存されている項目の値を取得して、すべてが`null`であることを確認できれば完了です。

## 3. プロバイダをテストする
次にプロバイダーをテストしてみます。
ファイル作成の準備については割愛します。

今回はshared_preferencesに４つの設定を保存しています。
その４つの設定の状態をプロバイダーにして保存しています。
プロバイダーはWidgetTestをする際に必然的にテストすることが多いです。
そのため単体でテストすることは少ないですが、今回はあえてやってみようと思います。

今回は４つのプロバイダーのうち、アイコンの設定を管理する`iconSettingProvider`についてのテスト方法について解説していきます。

### 3-1. mockitoの導入

プロバイダーのテストを行うときに注意したいのがこのテストはあくまでプロバイダーのテストであって、
shared_preferencesやkey_value_repositoryのテストではないということです。
どういうことかというと、実装をまずはみてみます。

```lib/data/repositories/key_value_repository/provider.dart

/// アイコン設定の値を提供するStreamを生成
/// `IconSettingRef`を通じてリポジトリにアクセスし、現在のアイコン設定を取得し、
/// その後、アイコン設定が変更されるたびに新しい値を提供する
@riverpod
Stream<bool?> iconSetting(IconSettingRef ref) async* {
  // キー値リポジトリのプロバイダーからリポジトリオブジェクトを取得
  final repository = ref.read(keyValueRepositoryProvider);

  // 最初のアイコン設定の値を取得し、yieldを使用してStreamに出力
  yield await repository.getIconSetting();

  // リポジトリの値変更通知を購読し、アイコン設定キーの変更のみにフィルターをかける
  await for (final _ in repository.onValueChange
      .where((key) => key == KeyValueRepository.iconSettingKey)) {
    // アイコン設定キーが変更されたとき、新しいアイコン設定の値を取得し、yieldでStreamに出力
    yield await repository.getIconSetting();
  }
}

```

上記の実装の中で`keyValueRepositoryProvider`を使って`KeyValueRepository`にアクセスしてメソッドを呼び出しています。
ただ、この中でたとえば`getIconSetting()`が正しく機能しているかはテストの必要がありません。
なぜなら今回のテストしたい内容は以下だからです。

**`iconSettingProvider`は設定の変更をストリームで配信することができる**

今回のように他のクラスの機能が絡んでくる場合にはそのクラスを**モック**します。
モックする、とはあるクラスの機能を模倣することです。
たとえば上記の`getIconSetting()`メソッであればこのメソッドを実行したら指定したアイコンの設定を返すように模倣します。
こうすることで`KeyValueRepository`の実装に依存せず、iconSettingProviderのテストを行うことができます。

そして、そのモックを簡単に生成してくれるのがmockitoというパッケージです。

https://pub.dev/packages/mockito

尚、mockitoについてより深く知りたい方は以下の記事をご覧ください。

https://zenn.dev/ncdc/articles/flutter_unit_test_mock

#### 1. mockitoをインストール
```shell

flutter pub add mockito

```
まずは上記のコマンドを実行してプロジェクトにmockitoをインストールしましょう。
#### 2. モック対象を指定して自動生成
```test/data/repositories/key_value_repository/provider_test.dart

/// モッククラスを生成するアノテーション
@GenerateNiceMocks([
  MockSpec<KeyValueRepositoryBase>(),
])

```
次にテストファイルの最初に上記のように書きます。
アノテーションで`@GenerateNiceMocks`とし、モックしたいクラスを配列で渡します。
今回は一つだけですが、カンマ区切りで複数渡すことができます。
この時、モックするクラスは`MockSpec`型で渡します。
そして型アノテーションでは今回は依存性の注入を行なっているので、`KeyValueRepositoryBase`を指定します。

準備ができたらいつもの自動生成コマンを実行します。

```shell

flutter pub run build_runner build --delete-conflicting-outputs

```

すると`provider_test.mocks.dart`というファイルが生成されます。

![スクリーンショット 2024-07-07 14.46.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058346/6738ecf9-2f7a-831c-20bc-118915d793d9.png)

### 3-2. 定数&変数、setUp、tearDownの設定

```test/data/repositories/key_value_repository/provider_test.dart

/// モッククラスを生成するアノテーション
@GenerateNiceMocks([
  MockSpec<KeyValueRepositoryBase>(),
])

/// テストはメイン関数の中で行う
void main() {
  // lateを使うことでここで宣言した値に各テストトランザクションからアクセスできる

  /// Riverpodを使用しているので、ProviderContainerを経由する
  late ProviderContainer container;

  /// mockしたMockKeyValueRepositoryBaseを使って機能にアクセスする
  late MockKeyValueRepositoryBase keyValueRepository;

  /// keyValueRepositoryのonValueChangeをモックするためのStreamController
  late StreamController<String> onValueChangeController;

  group('key_value_repository_provider test', () {
    /// テスト前の設定をここで行う
    setUp(() {
      // モックリポジトリのインスタンスを作成
      keyValueRepository = MockKeyValueRepositoryBase();
      // ストリームコントローラーを作成
      onValueChangeController = StreamController<String>();
      // プロバイダーのコンテナを作成し、リポジトリをオーバーライド
      container = ProviderContainer(
        overrides: [
          keyValueRepositoryProvider.overrideWithValue(keyValueRepository),
        ],
      );

      // keyValueRepositoryのonValueChangeはStreamだが、
      // そのStreamをテスト内のonValueChangeControllerに差し替えている
      // これにより、onValueChangeのイベントをテスト内で制御できるようにする
      when(keyValueRepository.onValueChange)
          .thenAnswer((_) => onValueChangeController.stream);
    });

    /// テスト後のクリーンアップを行う
    tearDown(() async {
      container.dispose();
      await onValueChangeController.close();
    });

    // 省略
  
```
repositoryのところと共通部分はありますが、差異がある点を説明します。

#### 変数＆定数

1. `late MockKeyValueRepositoryBase keyValueRepository;`
3-1で生成したmockクラスを変数で宣言します。

2. `late StreamController<String> onValueChangeController;`
`keyValueRepository`の`onValueChange`はストリームです。そのストリームを模倣して流すためにコントローラーを用意します。

#### setUp
まずは前半部分です。
```test/data/repositories/key_value_repository/provider_test.dart

    setUp(() {
      // モックリポジトリのインスタンスを作成
      keyValueRepository = MockKeyValueRepositoryBase();
      // ストリームコントローラーを作成
      onValueChangeController = StreamController<String>();
      // プロバイダーのコンテナを作成し、リポジトリをオーバーライド
      container = ProviderContainer(
        overrides: [
          keyValueRepositoryProvider.overrideWithValue(keyValueRepository),
        ],
      );
    
```
それぞれの変数にインスタンスを代入しています。
この時`container`には`ProviderContainer`を代入するが、その際に`overrides`で代入するプロバイダーを上書きします。
ここで`keyValueRepositoryProvider.overrideWithValue`によって返却しているのが`keyValueRepository`です。

つまり本来の実装では`KeyValueRepository`が返されますが、ここでは`MockKeyValueRepositoryBase`
を返すように差し替えています。

次に後半部分です。
```test/data/repositories/key_value_repository/provider_test.dart

      // keyValueRepositoryのonValueChangeはStreamだが、
      // そのStreamをテスト内のonValueChangeControllerに差し替えている
      // これにより、onValueChangeのイベントをテスト内で制御できるようにする
      when(keyValueRepository.onValueChange)
          .thenAnswer((_) => onValueChangeController.stream);

```

`when()`メソッドはmockitoの機能で、モック対象のスタブを作成する機能です。
スタブとは、特定のメソッドが呼び出されたときに、特定の動作をさせるためのものです。

上記では`keyValueRepository.onValueChange`が呼ばれたときに、`onValueChangeController.stream`を返すようにスタブが設定されています。
つまり実際のストリームではなく、テスト内で制御可能な`onValueChangeController.stream`が返されるようになります。

この方法により、テスト内でストリームのイベントを手動でトリガーして、特定のシナリオをシミュレートすることができます。

#### tearDown

```test/data/repositories/key_value_repository/provider_test.dart

 /// テスト後のクリーンアップを行う
    tearDown(() async {
      container.dispose();
      await onValueChangeController.close();
    });
    
```
`await onValueChangeController.close();`でストリームを閉じています。
ここで閉じておかないと、新しいストリームがテストごとに追加されてしまい、テストの実行速度に影響してしまうからです。

### 3-3. iconSettingProviderのテスト
#### 実装とテスト全体
まずはもう一度実装内容をおさらいしましょう。
```lib/data/repositories/key_value_repository/provider.dart

@riverpod
Stream<bool?> iconSetting(IconSettingRef ref) async* {
  // キー値リポジトリのプロバイダーからリポジトリオブジェクトを取得
  final repository = ref.read(keyValueRepositoryProvider);

  // 最初のアイコン設定の値を取得し、yieldを使用してStreamに出力
  yield await repository.getIconSetting();

  // リポジトリの値変更通知を購読し、アイコン設定キーの変更のみにフィルターをかける
  await for (final _ in repository.onValueChange
      .where((key) => key == KeyValueRepository.iconSettingKey)) {
    // アイコン設定キーが変更されたとき、新しいアイコン設定の値を取得し、yieldでStreamに出力
    yield await repository.getIconSetting();
  }
}

```

以下にテストの全体を載せておきます。
```test/data/repositories/key_value_repository/provider_test.dart

  test('iconSettingProviderは設定の変更をストリームで配信することができる', () async {
      // 初回のgetIconSettingをスタブ化
      when(keyValueRepository.getIconSetting())
          .thenAnswer((_) => Future.value(false));

      // 初期値を確認する
      var iconSetting = await container.read(iconSettingProvider.future);

      // 非同期の処理なので一旦待機させる
      await Future<void>.delayed(Duration.zero);

      // 左が検査対象、右が該当する結果
      expect(iconSetting, isFalse);

      // getIconSettingが１回呼ばれる
      verify(keyValueRepository.getIconSetting()).called(1);

      // trueに変更する
      // getIconSettingのスタブを上書きする
      when(keyValueRepository.getIconSetting())
          .thenAnswer((_) => Future.value(true));
      // ストリームを流す => await forの中の２回目のgetIconSettingが呼ばれる
      onValueChangeController.add(KeyValueRepository.iconSettingKey);

      // 現在のiconSettingの値を取り出す
      iconSetting = await container.read(iconSettingProvider.future);

      // 非同期の処理なので一旦待機させる
      await Future<void>.delayed(Duration.zero);

      // 設定が変わってtrueになる
      expect(iconSetting, isTrue);

      // getIconSettingが２回呼ばれる
      // １回目が初期値を読み込むときで、onValueChangeを検知して２回目のget
      verify(keyValueRepository.getIconSetting()).called(2);

      // ここで合計何回呼ばれたかを確認したいのであればgetIconSettingに対する
      // verifyはここでのみ呼び出す今回でいうとgetIconSettingに対して、
      // すでにverifyを２回使っているので合計値を出すことはできない

      // verify(keyValueRepository.getIconSetting()).called(3); // <= ❌
    });

```
ここはものすごく混乱するところなので、エディタ左右のどちらかに実装内容を展開して確認しながらテストを書いていきましょう。

![スクリーンショット 2024-07-07 15.43.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058346/1c58d303-d54e-84ee-2348-4894c20667c0.png)
*左がテスト、右が実装です*

#### 初期値を取得しての検証

実装内容の処理順番と内容を見ながらスタブを組み合わせて検証していきます。

まず最初の検証を行います。
実装を見てみると`iconSettingProvider`を購読すると、最初に初期値を取得して配信しています。

```lib/data/repositories/key_value_repository/provider.dart
 yield await repository.getIconSetting();
```
なのでまずは最初の`getIconSetting`を実行した値を`when()`でスタブ化します。
`getIconSetting`はFutrue型のメソッドで戻り値がboolです。
なので、返却する値を`.thenAnswer()`とい形で記述します。
コールバックで記述するため、初期値をfalseで設定した場合は`(_) => Future.value(false)`と記述します。

```test/data/repositories/key_value_repository/provider_test.dart
      // 初回のgetIconSettingをスタブ化
      when(keyValueRepository.getIconSetting())
          .thenAnswer((_) => Future.value(false));

      // 初期値を確認する
      var iconSetting = await container.read(iconSettingProvider.future);

      // 非同期の処理なので一旦待機させる
      await Future<void>.delayed(Duration.zero);

      // 左が検査対象、右が該当する結果
      expect(iconSetting, isFalse);

      // getIconSettingが１回呼ばれる
      verify(keyValueRepository.getIconSetting()).called(1);

```

ここで初期値を確認する際に`await container.read`としています。
`watch`や`listen`にしてしまうと、その時の値ではなく変化した最後の値になってしまい検証がしづらいため
`read`にします。
`iconSettingProvider.future`とすることで`AsyncValue`を剥がしてbool?の値を受け取ることができます。

そのあとすぐに`expect`で検証したいところですが、Stream型は値が反映されるまでに時間を要するため、
`await Future<void>.delayed(Duration.zero);`で値が反映されるまで待機させます。

`when()`でスタブ化したメソッドは検証が可能です。
よく使う検証が`verify().called()`メソッドです。
指定したメソッドが何回呼ばれているか？を検証するものです。

#### 設定が変更された場合を検証する

```test/data/repositories/key_value_repository/provider_test.dart

      // trueに変更する
      // getIconSettingのスタブを上書きする
      when(keyValueRepository.getIconSetting())
          .thenAnswer((_) => Future.value(true));
          
      // ストリームを流す => await forの中の２回目のgetIconSettingが呼ばれる
      onValueChangeController.add(KeyValueRepository.iconSettingKey);

      // 現在のiconSettingの値を取り出す
      iconSetting = await container.read(iconSettingProvider.future);

      // 非同期の処理なので一旦待機させる
      await Future<void>.delayed(Duration.zero);

      // 設定が変わってtrueになる
      expect(iconSetting, isTrue);

      // getIconSettingが２回呼ばれる
      // １回目が初期値を読み込むときで、onValueChangeを検知して２回目のget
      verify(keyValueRepository.getIconSetting()).called(2);

```
まずは前回同様`getIconSetting`メソッドのスタブを変更します。

そして、次がちょっと違うのですが`onValueChangeController.add(KeyValueRepository.iconSettingKey);`でストリームに値を流しています。
これは設定が**変わる**、ということはどこかでアイコン設定を保存する`setIconSetting`メソッドが実行されて、その結果`_onValueChanged.add(key);`が実行されたことを意味します。
この`key`の部分はその設定のキーであり、今回でいうところの`KeyValueRepository.iconSettingKey`です。
本来は`setIconSetting`メソッドが実行されると流れるようになっているストリームを手動で実行しています。

そして現在のアイコン設定を取り出して検証しています。
最後の`verify().called()`が２になっているのに注意です。
実装をみると、プロバイダーへアクセスした初回に`getIconSetting`を１回呼び出します。
次にonValueChangeを検知した際には`await for`で待機していた部分が実行され、２回目の`getIconSetting`が実行されています。

:::note info
verifyでの検証注意

コメントにも書いていますが、verifyは同一のメソッドに対して検証する場合はスタック性になっています。
このテスト内で`getIconSetting`メソッドは合計で３回呼ばれています。
今回のテストでは`getIconSetting`のverifyを２回実行し、それぞれ１回呼びれていることと、２回呼ばれていることというふうに３回を分割して１、２で検証しています。
基本はそのメソッドが呼ばれたタイミングでverifyを使って検証するのでこれでいいのすが、
もしも仮にこのテスト内で呼ばれた総合の数を検証したい場合はverify(keyValueRepository.getIconSetting())をテストの**最後にだけ使う**ようにしましょう。

:::

## 4. テストカバレッジを出してみよう

最後にテストカバレッジを出してみましょう。
このプロジェクトは`derry`コマンドを使って一発でカバレッジを出力することができます。
コマンドは以下です。


```shell

derry cov

```

しかし、こちらは下準備も必要です。derryが実行できる環境を自身のPCに設定する必要があります。
詳しい手順は以下の記事をご参照ください。

https://t.co/WUpYaWxG3i

### 全体のカバレッジ

![スクリーンショット 2024-07-07 17.00.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058346/7cafe858-9543-345f-b0a4-893c80e39576.png)

※ 全く書いていないものもまだあるので０％があります😰

### 今回の記事で書いたrepositoryとproviderのカバレッジ

![スクリーンショット 2024-07-07 17.01.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058346/1311fdb9-a859-9f9a-2025-3d8792cb9c25.png)

かなり全体を網羅したテストが書けたのではないでしょうか💪

### カバレッジどこまで埋めるの問題
これはそのプロジェクトチームによるので、その方針に合わせれば良いでしょう。
まず大前提として実装した時の仕様に合わせてテストケースを書きます。
その結果、目標のカバレッジに届いていない場合はテストケースが足りていないということになります。
私の現場ではディレクトリ単位で見て５０％を切らないようにするのを目標にしています。

## 終わりに

この記事では、Riverpodを使用した状態管理とDIの環境で、shared_preferencesを使ったリポジトリの
ユニットテストについて解説しました。

テストはコードの品質を保つために非常に重要です。
しかし、実務未経験であったり初学者にとっては非常に難しい内容だと思います。
今回の記事を通してユニットテストの学習のお役に立てれば幸いです。

何か疑問や質問があれば、ぜひお気軽にコメントをお寄せください。
最後までお読みいただき、ありがとうございました。