flutter-add-widget-test

flutter-add-widget-test

热门

使用 `WidgetTester` 实现组件级测试,验证 UI 渲染与用户交互(点击、滚动、文本输入等)。适用于校验特定 Widget 是否正确展示数据并按预期响应事件。

2783Star
163Fork
更新于 2026/8/5
SKILL.md
只读
名称
flutter-add-widget-test
描述

使用 `WidgetTester` 实现组件级测试,验证 UI 渲染与用户交互(点击、滚动、文本输入等)。适用于校验特定 Widget 是否正确展示数据并按预期响应事件。

编写 Flutter Widget 测试

目录

环境配置与准备

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

  1. pubspec.yamldev_dependencies 配置项中添加 flutter_test 依赖。
  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 状态或存在性(例如 findsOneWidgetfindsNothingfindsNWidgets(2)matchesGoldenFile)。

工作流:实现 Widget 测试

在编写新的 Widget 测试时,可以复制下方 CheckList 跟踪开发进度。

任务进度

  • [ ] Step 1: 定义测试。 使用 testWidgets('测试描述', (WidgetTester tester) async { ... }) 创建测试用例。
  • [ ] Step 2: 构建 Widget。 调用 await tester.pumpWidget(MyWidget()) 渲染 UI。若 Widget 依赖上层主题或方向数据,需将其包裹在 MaterialAppDirectionality 中。
  • [ ] Step 3: 定位元素。 为目标 Widget 实例化 Finder 对象。
  • [ ] Step 4: 校验初始状态。 使用 expect(finder, matcher) 验证初始渲染是否符合预期。
  • [ ] Step 5: 模拟用户交互。 执行手势或输入操作(例如 await tester.tap(buttonFinder))。
  • [ ] Step 6: 重刷 Widget 树。 调用 await tester.pump()await tester.pumpAndSettle() 处理状态变更与 UI 刷新。
  • [ ] Step 7: 校验更新后的状态。 使用 expect() 验证交互后的 UI 变化。
  • [ ] Step 8: 运行测试并验证。 执行命令 flutter test test/your_test_file_test.dart
  • [ ] Step 9: 反馈循环。 检查测试输出 -> 排查未通过的匹配器 (Matcher) -> 调整 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. 构建 Widget
    await tester.pumpWidget(const TodoList());

    // 2. 校验初始状态
    expect(find.byType(ListTile), findsNothing);

    // 3. 在 TextField 中输入文本
    await tester.enterText(find.byType(TextField), 'Buy groceries');

    // 4. 点击添加按钮
    await tester.tap(find.byType(FloatingActionButton));

    // 5. 刷新 Widget 以响应新状态
    await tester.pump();

    // 6. 校验事项已成功添加
    expect(find.text('Buy groceries'), findsOneWidget);

    // 7. 滑动该事项将其删除/移除
    await tester.drag(find.byType(Dismissible), const Offset(500, 0));

    // 8. 持续刷新直至移除动画播放完毕
    await tester.pumpAndSettle();

    // 9. 校验事项已成功移除
    expect(find.text('Buy groceries'), findsNothing);
  });
}