SKILL.md
只读
名称
flutter-add-widget-test
描述
使用 `WidgetTester` 实现组件级测试,验证 UI 渲染和用户交互(点击、滚动、输入文本)。适用于验证特定 widget 显示正确数据并按预期响应事件。
编写 Flutter Widget 测试
目录
设置与配置
在编写 widget 测试之前,确保测试环境已正确配置。
- 将
flutter_test依赖添加到pubspec.yaml的dev_dependencies部分。 - 将所有测试文件放在项目根目录的
test/目录下。 - 所有测试文件名以
_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或Directionalitywidget 中。 - [ ] 步骤 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()断言。 - 如果测试标准状态变化(例如按钮点击):
- 调用
await tester.tap(finder)。 - 调用
await tester.pump()触发单帧重建。
- 调用
- 如果测试动画、过渡或异步 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)确保目标 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('添加并删除待办事项', (WidgetTester tester) async {
// 1. 构建 widget
await tester.pumpWidget(const TodoList());
// 2. 验证初始状态
expect(find.byType(ListTile), findsNothing);
// 3. 在 TextField 中输入文本
await tester.enterText(find.byType(TextField), '购买杂货');
// 4. 点击添加按钮
await tester.tap(find.byType(FloatingActionButton));
// 5. 重建 widget 以反映新状态
await tester.pump();
// 6. 验证项目已添加
expect(find.text('购买杂货'), findsOneWidget);
// 7. 滑动项目以消除
await tester.drag(find.byType(Dismissible), const Offset(500, 0));
// 8. 构建 widget 直到消除动画结束
await tester.pumpAndSettle();
// 9. 验证项目已移除
expect(find.text('购买杂货'), findsNothing);
});
}






