メインコンテンツに進む

providerのテスト

Riverpod API の重要な部分は、provider を単独でテストする能力です。

適切なテストスイートを作成するためには、いくつかの課題を克服する必要があります:

  • テストは state を共有すべきではありません。
    これは、新しいテストが前のテストの影響を受けないことを意味します.
  • テストは特定の機能をモックできる能力を提供し、望んだ state を実現します。
  • テスト環境は可能な限り実際の環境に近いものであるべきです。

幸いなことに、Riverpod はこれらの目標を達成することを容易にします。

テストの設定

Riverpod でテストを定義するとき、大きく 2 つのシナリオがあります:

  • ユニットテスト、通常は Flutter 依存関係がないテスト。
    これは provider の動作を単独でテストするときに便利です。
  • ウィジェットテスト、通常は Flutter 依存関係があるテスト。
    provider を使用するウィジェットの動作をテストするのに便利です。

ユニットテスト

ユニットテストは package:testtest 関数を使用して定義されます。

他のテストとの主な違いは、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_testtestWidgets関数を使用して定義されます。

この場合、通常のウィジェットテストとの最大の違いは、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またはProviderContaineroverridesパラメータを指定することで可能です。

次の 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');
},
);

次に、これをmockitomocktailなどのパッケージと組み合わせて、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;
});
}