SKILL.md
只读
名称
flutter-add-integration-test
描述
配置 Flutter Driver 以实现应用交互,并将 MCP 操作转换为永久性集成测试。适用于为项目添加集成测试、通过 MCP 探索 UI 组件,或使用 integration_test 包自动化用户流程。
实现 Flutter 集成测试
目录
项目设置与依赖
配置项目以支持集成测试和 Flutter Driver 扩展。
- 在
pubspec.yaml中添加所需的开发依赖:flutter pub add 'dev:integration_test:{"sdk":"flutter"}' flutter pub add 'dev:flutter_test:{"sdk":"flutter"}' - 在应用入口点(通常是
lib/main.dart或专用的lib/main_test.dart)中启用 Flutter Driver 扩展:- 导入
package:flutter_driver/driver_extension.dart。 - 在
runApp()之前调用enableFlutterDriverExtension();。
- 导入
- 为应用代码中的关键 widget 添加
Key参数(例如ValueKey('login_button')),以确保测试中能够可靠定位。
通过 MCP 进行交互式探索
使用 Dart/Flutter MCP 服务器工具,在编写静态测试之前交互式地探索和操作应用状态。
- 启动:执行
launch_app,参数为target: "lib/main_test.dart",以启动应用并获取 DTD URI。 - 检查:执行
get_widget_tree以发现可用的Key、Text节点和 widgetType。 - 交互:执行
tap、enter_text和scroll来模拟用户流程。 - 等待:在导航或触发动画时,始终执行
waitFor或通过get_health验证状态。 - 排查未挂载的 Widget:如果 widget 在树中未找到,可能是由于懒加载在
SliverList或ListView中。执行scroll或scrollIntoView强制 widget 挂载后再进行交互。
测试编写指南
使用 flutter_test API 范式来组织集成测试。
- 在项目根目录创建专用的
integration_test/目录。 - 所有测试文件使用
<name>_test.dart命名约定。 - 在
main()开头调用IntegrationTestWidgetsFlutterBinding.ensureInitialized();初始化绑定。 - 使用
await tester.pumpWidget(MyApp());加载应用 UI。 - 在
tester.tap()等交互后,使用await tester.pumpAndSettle();触发帧并等待动画完成。 - 使用
expect(find.byKey(ValueKey('foo')), findsOneWidget);或findsNothing断言 widget 可见性。 - 使用
await tester.scrollUntilVisible(itemFinder, 500.0, scrollable: listFinder);滚动到屏幕外的特定 widget。
针对旧版 flutter_driver 的条件逻辑:
- 如果维护或迁移旧版
flutter_driver测试,请使用driver.waitFor()、driver.waitForAbsent()、driver.tap()和driver.scroll()代替WidgetTesterAPI。
执行与性能分析
使用 flutter drive 命令执行测试。需要位于 test_driver/integration_test.dart 的主机驱动脚本,该脚本调用 integrationDriver()。
条件执行目标:
- 如果在 Chrome 上测试: 在单独的终端中启动
chromedriver --port=4444,然后运行:
flutter drive --driver=test_driver/integration_test.dart --target=integration_test/app_test.dart -d chrome - 如果进行无头 Web 测试: 使用
-d web-server运行。 - 如果在 Android(本地)上测试: 运行
flutter drive --driver=test_driver/integration_test.dart --target=integration_test/app_test.dart。 - 如果在 Firebase Test Lab(Android)上测试:
- 构建调试 APK:
flutter build apk --debug - 构建测试 APK:
./gradlew app:assembleAndroidTest - 将两个 APK 上传到 Firebase Test Lab 控制台。
- 构建调试 APK:
工作流:端到端集成测试
复制并遵循此清单以实现和验证集成测试。
- [ ] 任务进度:设置
- [ ] 将
integration_test和flutter_test添加到pubspec.yaml。 - [ ] 在应用入口点注入
enableFlutterDriverExtension()。 - [ ] 为目标 widget 分配
ValueKey。
- [ ] 将
- [ ] 任务进度:探索
- [ ] 通过 MCP 运行
launch_app。 - [ ] 使用
get_widget_tree映射 widget 树。 - [ ] 使用 MCP 工具(
tap、enter_text)验证交互路径。
- [ ] 通过 MCP 运行
- [ ] 任务进度:编写
- [ ] 创建
integration_test/app_test.dart。 - [ ] 使用
WidgetTesterAPI 编写测试用例。 - [ ] 创建包含
integrationDriver()的test_driver/integration_test.dart。
- [ ] 创建
- [ ] 任务进度:执行与反馈循环
- [ ] 运行
flutter drive --driver=test_driver/integration_test.dart --target=integration_test/app_test.dart。 - [ ] 反馈循环:检查测试输出 -> 如果出现
PumpAndSettleTimedOutException,检查是否存在无限动画 -> 如果 widget 未找到,添加scrollUntilVisible-> 重新运行测试直至通过。
- [ ] 运行
示例
标准集成测试(integration_test/app_test.dart)
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('端到端测试', () {
testWidgets('点击浮动操作按钮,验证计数器', (tester) async {
// 加载应用 widget。
await tester.pumpWidget(const MyApp());
// 验证计数器初始值为 0。
expect(find.text('0'), findsOneWidget);
// 找到要点击的浮动操作按钮。
final fab = find.byKey(const ValueKey('increment'));
// 模拟点击浮动操作按钮。
await tester.tap(fab);
// 触发帧并等待动画。
await tester.pumpAndSettle();
// 验证计数器增加了 1。
expect(find.text('1'), findsOneWidget);
});
});
}
主机驱动脚本(test_driver/integration_test.dart)
import 'package:integration_test/integration_test_driver.dart';
Future<void> main() => integrationDriver();
性能分析驱动脚本(test_driver/perf_driver.dart)
如果测试操作包裹在 binding.traceAction() 中以捕获性能指标,请使用此驱动脚本。
import 'package:flutter_driver/flutter_driver.dart' as driver;
import 'package:integration_test/integration_test_driver.dart';
Future<void> main() {
return integrationDriver(
responseDataCallback: (data) async {
if (data != null) {
final timeline = driver.Timeline.fromJson(
data['scrolling_timeline'] as Map<String, dynamic>,
);
final summary = driver.TimelineSummary.summarize(timeline);
await summary.writeTimelineToFile(
'scrolling_timeline',
pretty: true,
includeSummary: true,
);
}
},
);
}






