mcpField
F
flutter-add-widget-test

flutter-add-widget-test

github.com/dhruvanbhalaraテスト・品質3051 回インストール9 週間前に更新
リポジトリ

flutter-add-widget-test について

Flutter 開発者向けの Widget テスト追加ガイドです。Finder、操作、フレームの進行、BLoC テスト、よくある問題と手順をまとめています。

主な機能

  1. 01Widget テストの主要 API
  2. 02Key による Widget 検索
  3. 03タップ・入力・スクロール操作
  4. 04アニメーションと時間の進め方
  5. 05BLoC と Cubit のテスト
  6. 06よくある問題の調査手順

活用シーン

  1. 01操作による状態変化のテスト
  2. 02フォーム入力とエラーの検証
  3. 03長いリスト内の画面外項目のテスト
  4. 04アニメーションやレイアウトの失敗調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dhruvanbhalara/skills --skill flutter-add-widget-test

インストール後、Claude Code で/flutter-add-widget-testを入力して読み込まれたか確認します。

詳細

目次

セットアップ

  • flutter_test は SDK の依存関係であるため、pub add は不要です。
  • テストファイルの命名: test/<mirror_path>/<widget>_test.dart。
  • すべてのテストファイルは次のように始めます:
    import 'package:flutter/material.dart';
    import 'package:flutter_test/flutter_test.dart';
    
  • すべてのウィジェットテストには testWidgets('description', (WidgetTester tester) async { ... }) を使用します。
  • MediaQuery、Theme、Navigator のコンテキストを提供するため、テスト対象のウィジェットは常に MaterialApp でラップします。

コア API

API目的
tester.pumpWidget(widget)ウィジェットをテスト環境にレンダリングします
tester.pump()単一のフレームをトリガーします
tester.pump(Duration(...))指定した時間だけ進めます
tester.pumpAndSettle()すべてのアニメーションが完了するまで待機します
tester.tap(finder)タップジェスチャーをシミュレートします
tester.longPress(finder)長押しをシミュレートします
tester.enterText(finder, 'text')テキストフィールドに入力します
tester.drag(finder, Offset(dx, dy))ドラッグジェスチャーをシミュレートします
tester.scrollUntilVisible(finder, delta)ウィジェットが表示されるまでスクロールします

Finder パターン

Finder を使用してテストツリー内のウィジェットを特定します。安定性のために Key ベースの Finder を優先してください。

  • find.byKey(const ValueKey('login_button')): 推奨。リファクタリングに対して最も安定しています。
  • find.byType(ElevatedButton): ウィジェットの型による指定。複数のインスタンスが存在する場合は失敗します。
  • find.text('Submit'): 表示テキストによる指定。ローカライズされた文字列では避けてください。
  • find.byIcon(Icons.add): アイコンデータによる指定。
  • find.descendant(of: parentFinder, matching: childFinder): ネストされた検索。
  • find.ancestor(of: childFinder, matching: parentFinder): 逆方向の検索。

Key の命名規則: インタラクティブなウィジェットには Key('feature_action_id') 形式を使用します。

// Production code
ElevatedButton(
  key: const Key('login_submit_button'),
  onPressed: _onSubmit,
  child: const Text('Login'),
)

// Test code
final submitButton = find.byKey(const Key('login_submit_button'));
await tester.tap(submitButton);

インタラクションパターン

タップして状態変化を検証する
testWidgets('increments counter on tap', (tester) async {
  await tester.pumpWidget(const MaterialApp(home: CounterPage()));

  expect(find.text('0'), findsOneWidget);

  await tester.tap(find.byKey(const Key('increment_button')));
  await tester.pump();

  expect(find.text('1'), findsOneWidget);
});
テキストを入力してフォームを検証する
testWidgets('validates email field', (tester) async {
  await tester.pumpWidget(const MaterialApp(home: LoginForm()));

  await tester.enterText(find.byKey(const Key('email_field')), 'invalid');
  await tester.tap(find.byKey(const Key('submit_button')));
  await tester.pumpAndSettle();

  expect(find.text('Enter a valid email'), findsOneWidget);
});
画面外のウィジェットまでスクロールする
testWidgets('finds item in long list', (tester) async {
  await tester.pumpWidget(const MaterialApp(home: ItemListPage()));

  final listFinder = find.byType(Scrollable);
  final itemFinder = find.byKey(const Key('item_99'));

  await tester.scrollUntilVisible(itemFinder, 500.0, scrollable: listFinder);

  expect(itemFinder, findsOneWidget);
});

Pump 戦略

シナリオに応じて適切な pump メソッドを選択してください:

シナリオメソッド理由
単純な状態変化pump()単一のフレームで十分です
アニメーションの完了pumpAndSettle()すべてのフレームを待機します
時間指定のアニメーションpump(Duration(milliseconds: 300))特定の時間だけ進めます
無限アニメーション(例: CircularProgressIndicator)pump()pumpAndSettle() は タイムアウト します
デバウンスされた入力pump(Duration(milliseconds: 500))デバウンス期間を待機します

WARNING: pumpAndSettle() は無限アニメーションで PumpAndSettleTimedOutException をスローします。ローディング状態をテストする場合は代わりに pump() を使用してください。

BLoC を使ったテスト

BLoC/Cubit に依存するウィジェットをテストする場合:

testWidgets('shows user name from BLoC', (tester) async {
  final mockBloc = MockUserBloc();

  whenListen(
    mockBloc,
    Stream.fromIterable([UserLoaded(User(name: 'Alice'))]),
    initialState: UserInitial(),
  );

  await tester.pumpWidget(
    MaterialApp(
      home: BlocProvider<UserBloc>.value(
        value: mockBloc,
        child: const UserProfilePage(),
      ),
    ),
  );

  await tester.pumpAndSettle();

  expect(find.text('Alice'), findsOneWidget);
});
  • bloc_test パッケージの MockBloc / MockCubit を使用します。
  • whenListen() を使用して状態ストリームのレスポンスをスタブ化します。
  • BlocProvider.value() を使用してモックをウィジェットツリーに注入します。

よくある落とし穴

エラー原因修正方法
No MediaQuery widget ancestorMaterialApp ラッパーの欠如MaterialApp(home: ...) でラップします
A RenderFlex overflowedウィジェットがテストビューポートを超えていますSizedBox または Expanded で制約します
Vertical viewport was given unbounded height高さ制約のない ListViewSizedBox(height: 600) でラップします
ナビゲーション後にウィジェットが見つからないpumpAndSettle() の欠如ナビゲーション後に await tester.pumpAndSettle() を追加します
PumpAndSettleTimedOutException無限アニメーションが実行中pumpAndSettle() の代わりに pump() を使用します

ワークフロー: ウィジェットテストの追加

タスクの進捗
  • ステップ 1: 本番コード内のインタラクティブウィジェットに Key を追加します。
  • ステップ 2: test/<mirror_path>/<widget>_test.dart にテストファイルを作成します。
  • ステップ 3: ウィジェットを MaterialApp でラップし、tester.pumpWidget() を呼び出します。
  • ステップ 4: 適切なファインダーを使用します(byKey を推奨)。
  • ステップ 5: インタラクション(tap、enterText、drag)をシミュレートします。
  • ステップ 6: expect(finder, findsOneWidget) または状態チェックでアサートします。
  • ステップ 7: Golden Variant / State Matrix / Interaction Contract パターンを適用します(flutter-testing を参照)。
  • ステップ 8: flutter test test/path/to/widget_test.dart を実行します。
  • ステップ 9: フィードバックループ:失敗を修正 → グリーンになるまで再実行します。

例

最小限のウィジェットテスト
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/features/counter/counter_page.dart';

void main() {
  group('$CounterPage', () {
    testWidgets('renders initial counter value', (tester) async {
      await tester.pumpWidget(const MaterialApp(home: CounterPage()));

      expect(find.text('0'), findsOneWidget);
      expect(find.byType(FloatingActionButton), findsOneWidget);
    });

    testWidgets('increments counter when FAB is tapped', (tester) async {
      await tester.pumpWidget(const MaterialApp(home: CounterPage()));

      await tester.tap(find.byType(FloatingActionButton));
      await tester.pump();

      expect(find.text('1'), findsOneWidget);
    });
  });
}

コメント