Inlayを使った画面のテスト
package:inlay/testing.dartのフェイクを使って、ナビゲーションやストアを利用する画面をウィジェットテストする方法。
add-to-appモジュールの画面は、プラットフォームチャネル経由でホストとやり取りする2つの
シングルトン、InlayNavigatorとKeyValueStorageに依存しています。普通のtestWidgetsの
実行では向こう側にホストが存在しないため、すべての呼び出しがMissingPluginExceptionで
失敗します。package:inlay/は両方のインメモリフェイクを提供しており、
どちらのシングルトンもテスト用に差し替えられます。
import 'package:inlay/inlay.dart';
import 'package:inlay/testing.dart';
setUp(() {
KeyValueStorage.instance = FakeKeyValueStorage();
InlayNavigator.instance = FakeInlayNavigator();
});テスト対象の画面側は何も変わりません。本番環境とまったく同じように、
KeyValueStorage.を読み、InlayNavigator.を呼び出し続けます。
ナビゲーションのアサーション
FakeInlayNavigatorは境界をまたぐ代わりに、すべての呼び出しを記録します。
pushedRoutes-pushに渡された型付きのルート(呼び出し順)。pushedPages- すべてのディスパッチメソッドから渡された生のPageSettings。ネイティブルート、 ダイアログ、pushForResult()によるpushも確認できます。resultsByRouteId-*ForResultメソッド用にあらかじめ用意しておく結果で、routeIdをキーに します。エントリがない場合は、結果なしで閉じられた画面と同様にnullで解決されます。 生成されるキーは、FlutterルートではrouteName(パスのテンプレート)、ダイアログルートでもrouteName(キャメルケースのクラス名)です。ネイティブルートではrouteIdとして公開されます。popCount- コンテナのpop回数。maybePopは、可能であればFlutter内のルートを通常どおりpopします。
testWidgets('awaits the Counter result and shows it', (tester) async {
final navigator = FakeInlayNavigator();
InlayNavigator.instance = navigator;
// Script the typed result the fake hands back to pushForResult().
navigator.resultsByRouteId[CounterPage.routeName] =
CounterPage.encodeResult(7);
await tester.pumpWidget(
const MaterialApp(
home: GreetingScreen(name: 'Test', style: GreetingStyle.casual),
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Open Counter (new engine, await result)'));
await tester.pumpAndSettle();
expect(navigator.pushedPages.single.routeId, CounterPage.routeName);
expect(find.text('Counter returned: 7'), findsOneWidget);
});記録されたページは生成されたワイヤーフォーマットを使っているため、何かがpushされたことを
確認するだけでなく、より踏み込んだアサーションが可能です。記録されたPageSettingsをデコードし、
実際に境界をまたいでいたはずのパラメータを検証できます。このテストの完全版は
example_module/にあります。そこでは、GreetingScreenが
初期化時に読み込むpackage_info_plusもモックしています(最後のセクションを参照)。
ストアを使う画面のテスト
FakeKeyValueStorageは、本物のストレージと同じセマンティクスを持つインメモリのマップです。
これには、正しさにとって最も重要な部分も含まれます。実際のプラットフォームは自身への通知を
抑制するため、自分の書き込みは自分のストリームに通知されません。主な操作ポイントは3つです。
data- 内部の生のマップ。中身の確認や低レベルのシーディングに使います。- 生成されたストアを通じたシーディング - 本番コードと同じキーのエンコーディングを使うため、 状態を準備する自然な方法です。
simulateExternalChange(entries)- 別のエンジンやネイティブコードがストレージに書き込む役割を 演じます。エントリを適用してストリームを発火させます。これは、このisolateから見たエンジン間の 同期そのものです。
testWidgets('reacts to a change from native code', (tester) async {
final storage = FakeKeyValueStorage();
KeyValueStorage.instance = storage;
await UserPreferencesStore(storage, userId: '42').setDisplayName('Marcin');
await tester.pumpWidget(const MaterialApp(home: ProfileScreen(userId: '42')));
await tester.pumpAndSettle();
expect(find.text('Display name: Marcin'), findsOneWidget);
storage.simulateExternalChange([
StorageEntry(key: 'user_preferences/42/displayName', value: 'From native'),
]);
await tester.pumpAndSettle();
expect(find.text('Display name: From native'), findsOneWidget);
});このテストは、どちらの端にもプラットフォームを置かずに、リアクティブな連鎖全体(ストア、
InlayCubit、ウィジェットの再ビルド)をカバーします。
フェイクでカバーできないこと
プラグインは別の問題です。package_info_plusなどのプラグインを呼び出す画面では、引き続き
そのプラグイン自身のテスト用フック(PackageInfo.など)が必要です。
また、ネイティブ側がルートを正しくデコードできるかどうかは、ウィジェットテストではわかりません。
そのためには、Flutterの画面までまたぐ薄いネイティブUIテストが必要です。そのパターンについては
マルチエンジン構成でのプラグインを参照してください。
サンプルモジュールには両方の種類のテストが含まれています。example_module/のウィジェット
テストと、両ホストのネイティブUIテスト(iOSホストのXCUITestとAndroidホストのUiAutomatorテスト)です。