providerのテスト
Riverpod API の重要な部分は、provider を単独でテストする能力です。
適切なテストスイートを作成するためには、いくつかの課題を克服する必要があります:
- テストは state を共有すべきではありません。
これは、新しいテストが前のテストの影響を受けないことを意味します. - テストは特定の機能をモックできる能力を提供し、望んだ state を実現します。
- テスト環境は可能な限り実際の環境に近いものであるべきです。
幸いなことに、Riverpod はこれらの目標を達成することを容易にします。
テストの設定
Riverpod でテストを定義するとき、大きく 2 つのシナリオがあります:
- ユニットテスト、通常は Flutter 依存関係がないテスト。
これは provider の動作を単独でテストするときに便利です。 - ウィジェットテスト、通常は Flutter 依存関係があるテスト。
provider を使用するウィジェットの動作をテストするのに便利です。
ユニットテスト
ユニットテストは package:test のtest
関数を使用して定義されます。
他のテストとの主な違いは、ProviderContainer
オブジェクトを作成する必要があるという点です。
このオブジェクトは provider との対話することを可能にします。
ProviderContainer
オブジェクトの作成と破棄のためのテストユーティリティを作成することが推奨されます:
import 'package:riverpod/riverpod.dart';
import 'package:test/test.dart';
/// [ProviderContainer]を作成し、テスト終了時に自動破棄するテストユーティリティです。
ProviderContainer createContainer({
ProviderContainer? parent,
List<Override> overrides = const [],
List<ProviderObserver>? observers,
}) {
// ProviderContainerを作成し、オプションでパラメータを指定します。
final container = ProviderContainer(
parent: parent,
overrides: overrides,
observers: observers,
);
// テスト終了時、containerを破棄します。
addTearDown(container.dispose);
return container;
}
次に、このユーティリティを使用して test
を定義できます:
void main() {
test('Some description', () {
// このテストのためにProviderContainerを作成します。
// テスト間でのProviderContainerの共有はしてはいけません。
final container = createContainer();
// TODO: アプリのテストを行うためにcontainerを使用します。
expect(
container.read(provider),
equals('some value'),
);
});
}
ProviderContainer を持つことで、次の方法で provider を読み取ることができます:
container.read
を使用して provider の現在の値を読み取る。container.listen
を使用して provider をリッスンし、変更を通知する。
provider が自動破棄される場合は、container.read
の使用に注意してください。
provider がリッスンされていない場合、テストの途中でその状態が破棄される可能性があります。
その場合は、container.listen
の使用を検討してください。
この戻り値を使用すると、provider の現在の値を読み取ることができますが、テストの途中で provider が破棄されないことも保証されます:
final subscription = container.listen<String>(provider, (_, __) {});
expect(
// `container.read(provider)`と同等です。
// しかし、"subscription"が破棄されない限り、providerは破棄されません。
subscription.read(),
'Some value',
);
ウィジェットテスト
ウィジェットテストはpackage:flutter_testのtestWidgets
関数を使用して定義されます。
この場合、通常のウィジェットテストとの最大の違いは、tester.pumpWidget
のルートにProviderScope
ウィジェットを追加する必要があるという点です:
void main() {
testWidgets('Some description', (tester) async {
await tester.pumpWidget(
const ProviderScope(child: YourWidgetYouWantToTest()),
);
});
}
これは Flutter アプリで Riverpod を有効にする時と似ています。
次に、tester
を使用してウィジェットと対話できます。
または provider と対話したい場合は、ProviderContainer
を取得できます。
これは ProviderScope.containerOf(buildContext)
を使用して取得できます。
したがって、tester
を使用すると次のように書くことができます:
final element = tester.element(find.byType(YourWidgetYouWantToTest));
final container = ProviderScope.containerOf(element);
次に、これを使用して provider を読み取ることができます。以下は完全な例です:
void main() {
testWidgets('Some description', (tester) async {
await tester.pumpWidget(
const ProviderScope(child: YourWidgetYouWantToTest()),
);
final element = tester.element(find.byType(YourWidgetYouWantToTest));
final container = ProviderScope.containerOf(element);
// TODO: providersと対話する
expect(
container.read(provider),
'some value',
);
});
}
provider のモック
これまでに、テストの設定方法と provider と基本的なやりとりについて見てきました。
しかし、場合によっては provider をモック(mock)したいことがあります。
全ての provider は追加の設定なしでモックすることができます。
これは、、ProviderScope
またはProviderContainer
のoverrides
パラメータを指定することで可能です。
次の provider を考えてみましょう:
// providerの初期化
Future<String> example(Ref ref) async => 'Hello world';
これを次のようにモックできます:
// ユニットテストでは以前の "createContainer" ユーティリティを再利用します。
final container = createContainer(
// モックするproviderのリストを指定することができる:
overrides: [
// この場合 "exampleProvider"をモック(mock)化しています
exampleProvider.overrideWith((ref) {
// この関数はproviderの典型的な初期化関数です。
// ここで通常は "ref.watch"を呼び出し、初期状態を返します。
// デフォルトの "Hello world "をカスタム値に置き換えてみましょう。
// 次に `exampleProvider`とやりとりするとこの値が返されます。
return 'Hello from tests';
}),
],
);
// ProviderScopeを使ったウィジェットテストでも同じことができます:
await tester.pumpWidget(
ProviderScope(
// ProviderScopesには、まったく同じ "overrides "パラメーターがあります。
overrides: [
// 前述と同じです。
exampleProvider.overrideWith((ref) => 'Hello from tests'),
],
child: const YourWidgetYouWantToTest(),
),
);
provider の変更を監視する
テストでProviderContainer
を取得して、それを利用して provider を"listen"することができます:
container.listen<String>(
provider,
(previous, next) {
print('The provider changed from $previous to $next');
},
);
次に、これをmockitoやmocktailなどのパッケージと組み合わせて、verify
API を使用できます。
または、よりシンプルに全ての変更をリストに追加し、それをアサート(assert)することもできます。
非同期 provider の待機
Riverpod では、provider が Future/Stream を返す場合が非常に多いです。
その場合、テストでは非同期操作が完了するのを待つ必要があります。
その方法の一つは、プロバイダの.future
を読み取ることです:
// TODO: アプリのテストを行うためにcontainerを使用します。
// 期待値は非同期なので、"expectLater" を使うべきである。
await expectLater(
// "provider" の代わりに "provider.future"で読み取ります。
// これは非同期providerの場合に可能で、providerの値を決めるfutureを返します。
container.read(provider.future),
// futureが期待値で決まることを確認できます。
// あるいはエラーの場合 "throwsA"を使用できます。
completion('some value'),
);
Notifiers のモック
一般的には Notifiers をモックすることは推奨されません。
なぜなら Notifiers は自己インスタンス化できず、provider の一部としてのみ機能するためです。
代わりに、Notifiers のロジックに抽象化のレベルを導入し、その抽象化をモックすることを検討すべきです。
例えば、Notifier をモック化するよりも、Notifier がデータを取得するために使用する "repository"をモックすることができます。
それでも Notifier をモックしたい場合、特別な考慮が必要です。:
モックは元の Notifier ベースクラスをサブクラス化する必要があります:
インターフェースが壊れる可能性があるため、Notifier を "implement" することはできません。
そのため、Notifier をモックするときは、次のような mockito コードを書かないでください:
class MyNotifierMock with Mock implements MyNotifier {}
代わりに次のように書いて下さい:
class MyNotifier extends _$MyNotifier {
int build() => throw UnimplementedError();
}
// モックはNotifierのベースクラスをサブクラス化する必要があります。
class MyNotifierMock extends _$MyNotifier with Mock implements MyNotifier {}
これを機能させるためには、モックをモックする Notifier と同じファイルに配置する必要があります。
そうしないと、_$MyNotifier
クラスにアクセスできません。
次に、Notifier を使用するには次のようにします:
void main() {
test('Some description', () {
final container = createContainer(
// providerをオーバーライドして、モックNotifierを作成します。
overrides: [myNotifierProvider.overrideWith(MyNotifierMock.new)],
);
// 次にContainerを通してモックNotifierを取得します:
final notifier = container.read(myNotifierProvider.notifier);
// 本物のNotifierと同じようにやりとりできます:
notifier.state = 42;
});
}