はじめに
私の現場ではRiverpodを使った状態管理とDIで、レイヤードアーキテクチャ(またはリバーポッドアーキテクチャ)を採用しています。
メンバーにタスクが振られ、実装が完了したらテストコードも合わせて書いてプルリクエストを提出しています。
私は実務未経験で今回の現場に配属されましたが、そんな実務の経験を経てテストコードを書く技術も蓄積することができました。
この記事ではその中から、ユニットテスト(unit test)に焦点を当ててお話ししていきます。
題材としてはdata層に作成したshared_prefernsesのrepositoryをテストしていきます。
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を使ってローカルに4つの値を保存するアプリです。
- アイコンの設定:
bool
- 背景色の番号:
int
- タイトルの文字:
String
- JSONで複数の設定:
CustomSetting
/文字列からJSONに変換
また、赤文字のボタンをタップしてデータを初期化(リセット)できます。
ソースコード
実装内容について
今回は実装内容についての紹介ではなく、あくまでテストに関する内容のため詳細の説明は割愛します。
適時必要な部分は抜粋していきますが、不足する部分があるかもしれません。
ソースコードで確認していただくか、以下の記事でご確認ください。
ユニットテストの基礎について
今回はユニットテストの基本的な関数やテストの実行方法を理解している前提でお話しします。
もしもその部分がまだ不安な方は以前書いた記事がありますので、ぜひそちらをご覧ください。
1. フォルダとファイルの作成
こちらは【Flutter】ユニットテスト基本のき 2-1. フォルダとファイルの作成の転載になります。
テストのファイルは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 // <= 😀
スクショで見るとこのような形になります。
テストファイルは名称の末尾を必ず_test.dartとしないとテスト用のファイルだと認識されずにテストが実行できません。
ファイル作成後は必ず確認しましょう!
ファイルを手動で作っていってもいいですが、私は必要なディレクトリごとコピーして、
テストファイルに貼り付けます。
あとはファイルの名前の末尾に**_test**をつけて、ファイルの中身を一度全て削除してしまえば
そっくりそのままディレクトリ構造も作れてお手軽なのでおすすめです。
2. repository.dartをテストする
2-1. 実行環境の準備
まず初めにファイルの中身を準備していきます。
【Flutter】ユニットテスト基本のき 2-2. 実行環境で紹介したテンプレートをコピーして貼り付けます。
その上で今回のテストしたいグループの数、テストケースの数に合うように修正します。
エディタを分割して、左か右側に実装したコードを載せておくと内容を確認しながら書けるのでおすすめです。
今回の実装はlib/data/repositories/key_value_repository/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('全てのデータを初期化できる', () {});
});
}
グループに関してはエラーハンドリングや初期データを分けたテストなどはしていないため、1つにまとめています。
テストしたい内容は本来であればそのタスクの仕様に合わせて設定します。
つまり必ずしもメソッド単位というわけではありません。
今回は厳密に仕様を決めていないこともあるので、test()
のコメントにある通り保存と取得関連は対象のデータごとにそれぞれまとめ、合計6個のテストケースに分かれています。
2-2. 定数&変数、setUp、tearDownの設定
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
を経由するために使用します。
ProviderContainerについて
実は今回でいえばoverrides
で上書きすることはないので、本来は
final container = ProviderContainer();
で宣言しても良いです。
ただ大体の場合はsetUp()
関数内でoverrides
することが多いため
そのままにしています。
ここは慣れてきたらしっかりと使い分けてもいいかもしれません。
setUp
今回設定するのは3点です。
-
SharedPreferences.setMockInitialValues({})
主にエミュレータなどのデバイスデータに影響を与えないためテスト環境上にmockの保存先を作成します。
今回は初期値なしの状態でテストします。 -
container = ProviderContainer();
先ほど定数で宣言したcontainer
を設定。今回は特にないのでProviderContainer()
を代入するのみです。 -
keyValueRepository = container.read(keyValueRepositoryProvider);
実装を見てみるとわかるのですが、KeyValueRepositoryBase
はkeyValueRepositoryProvider
経由でアクセスできます。
container
経由でインスタンスを取得しています。
そしてkeyValueRepositoryProvider
が最終的に返却するのは
実際の実装を持っているKeyValueRepository(ref);
です。
ここら辺が少し複雑ですが、頑張って整理していきましょう。
/// `KeyValueRepositoryBase` のインスタンスを生成
@Riverpod(keepAlive: true)
KeyValueRepositoryBase keyValueRepository(KeyValueRepositoryRef ref) {
return KeyValueRepository(ref);
}
データベースのmock化
SharedPreferencesに限らずデータベース関連のパッケージであればテスト用のmock化が容易されていると思います。
まずは公式ドキュメントを確認しましょう。
tearDown()
container.dispose();
のみです。
テスト用に作成したProviderContainerを破棄してリソースを解放します。
毎回破棄することでインスタンスの重複を防止して不要なメモリの消費を防ぎます。
これで基本の準備、設定は完了です。次からテストを書いていきます。
2-3. onValueChangeのテスト
以下に関連する実装部分を抜粋しています。
/// アイコン設定のキー
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('値の変更を監視することができる', () 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. 取得と保存のテスト
今回は4種類のデータの保存と取得メソッドがありますが、どれもほとんど同じですのでここでは「アイコンの設定を取得し、保存できる」、のテストについて解説します。
以下、実装の抜粋です。
@override
Future<bool?> getIconSetting() => _get(iconSettingKey);
@override
Future<void> setIconSetting({bool? value}) => _set(iconSettingKey, value);
以下がテスト内容です。
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. データの初期化のテスト
いかが実装部分です。
@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('全てのデータを初期化できる', () 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);
});
内容は今まで一番長いですが、実は保存と取得する項目が増えただけです。
まず最初に保存系4つのメソッド(setXxxx()
)を実行して値を保存し、
保存されていることを確認します。
その後、await keyValueRepository.initData();
を実行して、データをすべて消します。
最後にすべての保存されている項目の値を取得して、すべてがnull
であることを確認できれば完了です。
3. プロバイダをテストする
次にプロバイダーをテストしてみます。
ファイル作成の準備については割愛します。
今回はshared_preferencesに4つの設定を保存しています。
その4つの設定の状態をプロバイダーにして保存しています。
プロバイダーはWidgetTestをする際に必然的にテストすることが多いです。
そのため単体でテストすることは少ないですが、今回はあえてやってみようと思います。
今回は4つのプロバイダーのうち、アイコンの設定を管理するiconSettingProvider
についてのテスト方法について解説していきます。
3-1. mockitoの導入
プロバイダーのテストを行うときに注意したいのがこのテストはあくまでプロバイダーのテストであって、
shared_preferencesやkey_value_repositoryのテストではないということです。
どういうことかというと、実装をまずはみてみます。
/// アイコン設定の値を提供する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というパッケージです。
尚、mockitoについてより深く知りたい方は以下の記事をご覧ください。
1. mockitoをインストール
flutter pub add mockito
まずは上記のコマンドを実行してプロジェクトにmockitoをインストールしましょう。
2. モック対象を指定して自動生成
/// モッククラスを生成するアノテーション
@GenerateNiceMocks([
MockSpec<KeyValueRepositoryBase>(),
])
次にテストファイルの最初に上記のように書きます。
アノテーションで@GenerateNiceMocks
とし、モックしたいクラスを配列で渡します。
今回は一つだけですが、カンマ区切りで複数渡すことができます。
この時、モックするクラスはMockSpec
型で渡します。
そして型アノテーションでは今回は依存性の注入を行なっているので、KeyValueRepositoryBase
を指定します。
準備ができたらいつもの自動生成コマンを実行します。
flutter pub run build_runner build --delete-conflicting-outputs
するとprovider_test.mocks.dart
というファイルが生成されます。
3-2. 定数&変数、setUp、tearDownの設定
/// モッククラスを生成するアノテーション
@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のところと共通部分はありますが、差異がある点を説明します。
変数&定数
-
late MockKeyValueRepositoryBase keyValueRepository;
3-1で生成したmockクラスを変数で宣言します。 -
late StreamController<String> onValueChangeController;
keyValueRepository
のonValueChange
はストリームです。そのストリームを模倣して流すためにコントローラーを用意します。
setUp
まずは前半部分です。
setUp(() {
// モックリポジトリのインスタンスを作成
keyValueRepository = MockKeyValueRepositoryBase();
// ストリームコントローラーを作成
onValueChangeController = StreamController<String>();
// プロバイダーのコンテナを作成し、リポジトリをオーバーライド
container = ProviderContainer(
overrides: [
keyValueRepositoryProvider.overrideWithValue(keyValueRepository),
],
);
それぞれの変数にインスタンスを代入しています。
この時container
にはProviderContainer
を代入するが、その際にoverrides
で代入するプロバイダーを上書きします。
ここでkeyValueRepositoryProvider.overrideWithValue
によって返却しているのがkeyValueRepository
です。
つまり本来の実装ではKeyValueRepository
が返されますが、ここではMockKeyValueRepositoryBase
を返すように差し替えています。
次に後半部分です。
// keyValueRepositoryのonValueChangeはStreamだが、
// そのStreamをテスト内のonValueChangeControllerに差し替えている
// これにより、onValueChangeのイベントをテスト内で制御できるようにする
when(keyValueRepository.onValueChange)
.thenAnswer((_) => onValueChangeController.stream);
when()
メソッドはmockitoの機能で、モック対象のスタブを作成する機能です。
スタブとは、特定のメソッドが呼び出されたときに、特定の動作をさせるためのものです。
上記ではkeyValueRepository.onValueChange
が呼ばれたときに、onValueChangeController.stream
を返すようにスタブが設定されています。
つまり実際のストリームではなく、テスト内で制御可能なonValueChangeController.stream
が返されるようになります。
この方法により、テスト内でストリームのイベントを手動でトリガーして、特定のシナリオをシミュレートすることができます。
tearDown
/// テスト後のクリーンアップを行う
tearDown(() async {
container.dispose();
await onValueChangeController.close();
});
await onValueChangeController.close();
でストリームを閉じています。
ここで閉じておかないと、新しいストリームがテストごとに追加されてしまい、テストの実行速度に影響してしまうからです。
3-3. iconSettingProviderのテスト
実装とテスト全体
まずはもう一度実装内容をおさらいしましょう。
@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('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が1回呼ばれる
verify(keyValueRepository.getIconSetting()).called(1);
// trueに変更する
// getIconSettingのスタブを上書きする
when(keyValueRepository.getIconSetting())
.thenAnswer((_) => Future.value(true));
// ストリームを流す => await forの中の2回目のgetIconSettingが呼ばれる
onValueChangeController.add(KeyValueRepository.iconSettingKey);
// 現在のiconSettingの値を取り出す
iconSetting = await container.read(iconSettingProvider.future);
// 非同期の処理なので一旦待機させる
await Future<void>.delayed(Duration.zero);
// 設定が変わってtrueになる
expect(iconSetting, isTrue);
// getIconSettingが2回呼ばれる
// 1回目が初期値を読み込むときで、onValueChangeを検知して2回目のget
verify(keyValueRepository.getIconSetting()).called(2);
// ここで合計何回呼ばれたかを確認したいのであればgetIconSettingに対する
// verifyはここでのみ呼び出す今回でいうとgetIconSettingに対して、
// すでにverifyを2回使っているので合計値を出すことはできない
// verify(keyValueRepository.getIconSetting()).called(3); // <= ❌
});
ここはものすごく混乱するところなので、エディタ左右のどちらかに実装内容を展開して確認しながらテストを書いていきましょう。
初期値を取得しての検証
実装内容の処理順番と内容を見ながらスタブを組み合わせて検証していきます。
まず最初の検証を行います。
実装を見てみるとiconSettingProvider
を購読すると、最初に初期値を取得して配信しています。
yield await repository.getIconSetting();
なのでまずは最初のgetIconSetting
を実行した値をwhen()
でスタブ化します。
getIconSetting
はFutrue型のメソッドで戻り値がboolです。
なので、返却する値を.thenAnswer()
とい形で記述します。
コールバックで記述するため、初期値をfalseで設定した場合は(_) => Future.value(false)
と記述します。
// 初回の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が1回呼ばれる
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()
メソッドです。
指定したメソッドが何回呼ばれているか?を検証するものです。
設定が変更された場合を検証する
// trueに変更する
// getIconSettingのスタブを上書きする
when(keyValueRepository.getIconSetting())
.thenAnswer((_) => Future.value(true));
// ストリームを流す => await forの中の2回目のgetIconSettingが呼ばれる
onValueChangeController.add(KeyValueRepository.iconSettingKey);
// 現在のiconSettingの値を取り出す
iconSetting = await container.read(iconSettingProvider.future);
// 非同期の処理なので一旦待機させる
await Future<void>.delayed(Duration.zero);
// 設定が変わってtrueになる
expect(iconSetting, isTrue);
// getIconSettingが2回呼ばれる
// 1回目が初期値を読み込むときで、onValueChangeを検知して2回目のget
verify(keyValueRepository.getIconSetting()).called(2);
まずは前回同様getIconSetting
メソッドのスタブを変更します。
そして、次がちょっと違うのですがonValueChangeController.add(KeyValueRepository.iconSettingKey);
でストリームに値を流しています。
これは設定が変わる、ということはどこかでアイコン設定を保存するsetIconSetting
メソッドが実行されて、その結果_onValueChanged.add(key);
が実行されたことを意味します。
このkey
の部分はその設定のキーであり、今回でいうところのKeyValueRepository.iconSettingKey
です。
本来はsetIconSetting
メソッドが実行されると流れるようになっているストリームを手動で実行しています。
そして現在のアイコン設定を取り出して検証しています。
最後のverify().called()
が2になっているのに注意です。
実装をみると、プロバイダーへアクセスした初回にgetIconSetting
を1回呼び出します。
次にonValueChangeを検知した際にはawait for
で待機していた部分が実行され、2回目のgetIconSetting
が実行されています。
verifyでの検証注意
コメントにも書いていますが、verifyは同一のメソッドに対して検証する場合はスタック性になっています。
このテスト内でgetIconSetting
メソッドは合計で3回呼ばれています。
今回のテストではgetIconSetting
のverifyを2回実行し、それぞれ1回呼びれていることと、2回呼ばれていることというふうに3回を分割して1、2で検証しています。
基本はそのメソッドが呼ばれたタイミングでverifyを使って検証するのでこれでいいのすが、
もしも仮にこのテスト内で呼ばれた総合の数を検証したい場合はverify(keyValueRepository.getIconSetting())をテストの最後にだけ使うようにしましょう。
4. テストカバレッジを出してみよう
最後にテストカバレッジを出してみましょう。
このプロジェクトはderry
コマンドを使って一発でカバレッジを出力することができます。
コマンドは以下です。
derry cov
しかし、こちらは下準備も必要です。derryが実行できる環境を自身のPCに設定する必要があります。
詳しい手順は以下の記事をご参照ください。
全体のカバレッジ
※ 全く書いていないものもまだあるので0%があります😰
今回の記事で書いたrepositoryとproviderのカバレッジ
かなり全体を網羅したテストが書けたのではないでしょうか💪
カバレッジどこまで埋めるの問題
これはそのプロジェクトチームによるので、その方針に合わせれば良いでしょう。
まず大前提として実装した時の仕様に合わせてテストケースを書きます。
その結果、目標のカバレッジに届いていない場合はテストケースが足りていないということになります。
私の現場ではディレクトリ単位で見て50%を切らないようにするのを目標にしています。
終わりに
この記事では、Riverpodを使用した状態管理とDIの環境で、shared_preferencesを使ったリポジトリの
ユニットテストについて解説しました。
テストはコードの品質を保つために非常に重要です。
しかし、実務未経験であったり初学者にとっては非常に難しい内容だと思います。
今回の記事を通してユニットテストの学習のお役に立てれば幸いです。
何か疑問や質問があれば、ぜひお気軽にコメントをお寄せください。
最後までお読みいただき、ありがとうございました。