Inlayを使った画面のテスト

package:inlay/testing.dartのフェイクを使って、ナビゲーションやストアを利用する画面をウィジェットテストする方法。

add-to-appモジュールの画面は、プラットフォームチャネル経由でホストとやり取りする2つの シングルトン、InlayNavigatorとKeyValueStorageに依存しています。普通のtestWidgetsの 実行では向こう側にホストが存在しないため、すべての呼び出しがMissingPluginExceptionで 失敗します。package:inlay/testing.dartは両方のインメモリフェイクを提供しており、 どちらのシングルトンもテスト用に差し替えられます。

import 'package:inlay/inlay.dart';
import 'package:inlay/testing.dart';

setUp(() {
  KeyValueStorage.instance = FakeKeyValueStorage();
  InlayNavigator.instance = FakeInlayNavigator();
});

テスト対象の画面側は何も変わりません。本番環境とまったく同じように、 KeyValueStorage.instanceを読み、InlayNavigator.instance.push(...)を呼び出し続けます。

ナビゲーションのアサーション

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/test/greeting_screen_test.dartにあります。そこでは、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.setMockInitialValues(...)など)が必要です。 また、ネイティブ側がルートを正しくデコードできるかどうかは、ウィジェットテストではわかりません。 そのためには、Flutterの画面までまたぐ薄いネイティブUIテストが必要です。そのパターンについては マルチエンジン構成でのプラグインを参照してください。 サンプルモジュールには両方の種類のテストが含まれています。example_module/test/のウィジェット テストと、両ホストのネイティブUIテスト(iOSホストのXCUITestとAndroidホストのUiAutomatorテスト)です。

プライバシーポリシー

このページの内容