mcpField
F
flutter-add-widget-test

flutter-add-widget-test

github.com/flutter测试与质量3.1k39.7k 次安装2 天前更新
代码仓库

关于 flutter-add-widget-test

为 Flutter 开发者提供编写 Widget 测试的指导,涵盖测试环境、组件查找、断言和交互流程。

主要功能

  1. 01配置 flutter_test 与测试目录
  2. 02使用 Finder 定位组件
  3. 03验证组件状态与渲染
  4. 04模拟点击、输入和滚动
  5. 05处理动画及状态更新

适用场景

  1. 01为静态界面编写渲染测试
  2. 02验证按钮操作后的界面变化
  3. 03测试输入框和长列表交互

安装

复制下面的命令,在终端中运行。

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

安装后在 Claude Code 中输入/flutter-add-widget-test确认已加载。

详细介绍

编写 Flutter Widget 测试

目录

设置与配置

编写 Widget 测试前,请确保已正确配置测试环境。

  1. 将 flutter_test 依赖项添加到 pubspec.yaml 的 dev_dependencies 部分。
  2. 将所有测试文件放在项目根目录下的 test/ 目录中。
  3. 所有测试文件名均以 _test.dart 结尾(例如,widget_test.dart)。

核心组件

使用以下 flutter_test 组件与 Widget 树交互并对其进行验证:

  • WidgetTester:用于在测试环境中构建 Widget 并与其交互的主要接口。由 testWidgets() 函数自动提供。
  • Finder:用于在测试环境中定位 Widget(例如,find.text('Submit')、find.byType(TextField)、find.byKey(Key('submit_btn')))。
  • Matcher:用于验证 Finder 定位到的 Widget 是否存在或处于何种状态(例如,findsOneWidget、findsNothing、findsNWidgets(2)、matchesGoldenFile)。

工作流:实现 Widget 测试

复制以下检查清单,以跟踪实现新 Widget 测试的进度。

任务进度
  • 步骤 1:定义测试。 使用 testWidgets('description', (WidgetTester tester) async { ... })。
  • 步骤 2:构建 Widget。 调用 await tester.pumpWidget(MyWidget()) 来渲染 UI。如果 Widget 需要继承的方向性或主题数据,请将其包装在 MaterialApp 或 Directionality Widget 中。
  • 步骤 3:定位元素。 为目标 Widget 实例化 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:反馈循环。 检查测试输出 -> 找出失败的匹配器 -> 调整 Widget 逻辑或测试断言 -> 重新运行,直到测试通过。

交互与状态管理

根据所测试的交互或状态变化类型,应用以下条件逻辑:

  • 如果测试静态渲染: 调用一次 await tester.pumpWidget(),然后立即运行 expect() 断言。
  • 如果测试常规状态变化(例如,按钮点击):
    1. 调用 await tester.tap(finder)。
    2. 调用 await tester.pump() 触发单帧重建。
  • 如果测试动画、过渡或异步 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),确保目标 Widget 已渲染,然后再与其交互。

示例

高保真 Widget 测试实现

目标 Widget(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