mcpField
F
flutter-add-widget-test

flutter-add-widget-test

github.com/flutterテスト・品質3.1k39.7k 回インストール2 日前に更新
リポジトリ

flutter-add-widget-test について

Flutter 開発者向けに、Widget テストの環境設定から検索、検証、操作までを案内します。

主な機能

  1. 01flutter_test とテスト配置の設定
  2. 02Finder による Widget の特定
  3. 03状態と描画の検証
  4. 04タップ・入力・スクロールの再現
  5. 05アニメーションと状態更新への対応

活用シーン

  1. 01静的 UI の描画テスト
  2. 02ボタン操作後の UI 変更の検証
  3. 03テキスト入力欄や長いリストのテスト

インストール

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

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

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

詳細

Flutter ウィジェットテストの作成

目次

セットアップと設定

ウィジェットテストを作成する前に、テスト環境が適切に設定されていることを確認してください。

  1. pubspec.yaml の dev_dependencies セクションに flutter_test 依存関係を追加します。
  2. すべてのテストファイルを、プロジェクトルートの test/ ディレクトリに配置します。
  3. すべてのテストファイル名の末尾に _test.dart を付けます(例:widget_test.dart)。

主要コンポーネント

ウィジェットツリーとの操作や検証には、以下の flutter_test コンポーネントを使用します。

  • WidgetTester:テスト環境でウィジェットを構築し、操作するための主要なインターフェースです。testWidgets() 関数によって自動的に提供されます。
  • Finder:テスト環境でウィジェットを検索します(例:find.text('Submit')、find.byType(TextField)、find.byKey(Key('submit_btn')))。
  • Matcher:Finder で見つかったウィジェットの存在や状態を検証します(例:findsOneWidget、findsNothing、findsNWidgets(2)、matchesGoldenFile)。

ワークフロー:ウィジェットテストの実装

新しいウィジェットテストを実装する際に進捗を確認できるよう、以下のチェックリストをコピーしてください。

タスクの進捗
  • ステップ 1:テストを定義します。 testWidgets('description', (WidgetTester tester) async { ... }) を使用します。
  • ステップ 2:ウィジェットを構築します。 await tester.pumpWidget(MyWidget()) を呼び出して UI を描画します。継承される方向データやテーマデータが必要な場合は、ウィジェットを MaterialApp または Directionality ウィジェットでラップします。
  • ステップ 3:要素を検索します。 対象のウィジェット用に Finder オブジェクトをインスタンス化します。
  • ステップ 4:初期状態を検証します。 expect(finder, matcher) を使用して初期描画を検証します。
  • ステップ 5:操作をシミュレートします。 ジェスチャーや入力を実行します(例:await tester.tap(buttonFinder))。
  • ステップ 6:ツリーを再構築します。 await tester.pump() または await tester.pumpAndSettle() を呼び出して、状態の変化を処理します。
  • ステップ 7:更新後の状態を検証します。 expect() を使用して、操作後の UI を検証します。
  • ステップ 8:実行して検証します。 flutter test test/your_test_file_test.dart を実行します。
  • ステップ 9:フィードバックループ。 テスト出力を確認する → 失敗したマッチャーを特定する → ウィジェットのロジックまたはテストのアサーションを調整する → テストが成功するまで再実行します。

操作と状態管理

テスト対象の操作や状態変化に応じて、以下の条件分岐ロジックを適用します。

  • 静的な描画をテストする場合: await tester.pumpWidget() を一度呼び出し、その直後に expect() アサーションを実行します。
  • 通常の状態変化(ボタンのタップなど)をテストする場合:
    1. await tester.tap(finder) を呼び出します。
    2. await tester.pump() を呼び出して、1 フレームの再構築をトリガーします。
  • アニメーション、トランジション、または非同期の UI 更新をテストする場合:
    1. アクションをトリガーします(例:await tester.drag(finder, Offset(500, 0)))。
    2. これ以上フレームがスケジュールされなくなるまで(アニメーションが完了するまで)繰り返しフレームをポンプするため、await tester.pumpAndSettle() を呼び出します。
  • テキスト入力をテストする場合: await tester.enterText(textFieldFinder, 'Input string') を呼び出します。
  • 動的なリストや長いリストの項目をテストする場合: 対象のウィジェットが操作前に描画されるよう、await tester.scrollUntilVisible(itemFinder, 500.0, scrollable: listFinder) を呼び出します。

例

高忠実度なウィジェットテストの実装

対象ウィジェット(lib/todo_list.dart):

import 'package:flutter/material.dart';

class TodoList extends StatefulWidget {
  const TodoList({super.key});

  @override
  State<TodoList> createState() => _TodoListState();
}

class _TodoListState extends State<TodoList> {
  final todos = <String>[];
  final controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Column(
          children: [
            TextField(controller: controller),
            Expanded(
              child: ListView.builder(
                itemCount: todos.length,
                itemBuilder: (context, index) {
                  final todo = todos[index];
                  return Dismissible(
                    key: Key('$todo$index'),
                    onDismissed: (_) => setState(() => todos.removeAt(index)),
                    child: ListTile(title: Text(todo)),
                  );
                },
              ),
            ),
          ],
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            setState(() {
              todos.add(controller.text);
              controller.clear();
            });
          },
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}

テストの実装(test/todo_list_test.dart):

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/todo_list.dart';

void main() {
  testWidgets('Add and remove a todo item', (WidgetTester tester) async {
    // 1. Build the widget
    await tester.pumpWidget(const TodoList());

    // 2. Verify initial state
    expect(find.byType(ListTile), findsNothing);

    // 3. Enter text into the TextField
    await tester.enterText(find.byType(TextField), 'Buy groceries');

    // 4. Tap the add button
    await tester.tap(find.byType(FloatingActionButton));

    // 5. Rebuild the widget to reflect the new state
    await tester.pump();

    // 6. Verify the item was added
    expect(find.text('Buy groceries'), findsOneWidget);

    // 7. Swipe the item to dismiss it
    await tester.drag(find.byType(Dismissible), const Offset(500, 0));

    // 8. Build the widget until the dismiss animation ends
    await tester.pumpAndSettle();

    // 9. Verify the item was removed
    expect(find.text('Buy groceries'), findsNothing);
  });
}

コメント