flutter-add-widget-test について
Flutter 開発者向けの Widget テスト追加ガイドです。Finder、操作、フレームの進行、BLoC テスト、よくある問題と手順をまとめています。
主な機能
- 01Widget テストの主要 API
- 02Key による Widget 検索
- 03タップ・入力・スクロール操作
- 04アニメーションと時間の進め方
- 05BLoC と Cubit のテスト
- 06よくある問題の調査手順
活用シーン
- 01操作による状態変化のテスト
- 02フォーム入力とエラーの検証
- 03長いリスト内の画面外項目のテスト
- 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 ancestor | MaterialApp ラッパーの欠如 | MaterialApp(home: ...) でラップします |
A RenderFlex overflowed | ウィジェットがテストビューポートを超えています | SizedBox または Expanded で制約します |
Vertical viewport was given unbounded height | 高さ制約のない ListView | SizedBox(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);
});
});
}
コメント
Markdown 対応