flutter-add-widget-test について
Flutter 開発者向けに、Widget テストの環境設定から検索、検証、操作までを案内します。
主な機能
- 01flutter_test とテスト配置の設定
- 02Finder による Widget の特定
- 03状態と描画の検証
- 04タップ・入力・スクロールの再現
- 05アニメーションと状態更新への対応
活用シーン
- 01静的 UI の描画テスト
- 02ボタン操作後の UI 変更の検証
- 03テキスト入力欄や長いリストのテスト
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ flutter/ agent-plugins --skill flutter-add-widget-testインストール後、Claude Code で/flutter-add-widget-testを入力して読み込まれたか確認します。
詳細
Flutter ウィジェットテストの作成
目次
セットアップと設定
ウィジェットテストを作成する前に、テスト環境が適切に設定されていることを確認してください。
pubspec.yamlのdev_dependenciesセクションにflutter_test依存関係を追加します。- すべてのテストファイルを、プロジェクトルートの
test/ディレクトリに配置します。 - すべてのテストファイル名の末尾に
_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()アサーションを実行します。 - 通常の状態変化(ボタンのタップなど)をテストする場合:
await tester.tap(finder)を呼び出します。await tester.pump()を呼び出して、1 フレームの再構築をトリガーします。
- アニメーション、トランジション、または非同期の UI 更新をテストする場合:
- アクションをトリガーします(例:
await tester.drag(finder, Offset(500, 0)))。 - これ以上フレームがスケジュールされなくなるまで(アニメーションが完了するまで)繰り返しフレームをポンプするため、
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);
});
}
コメント
Markdown 対応