SKILL.md
readonlyread-only
name
flutter-add-integration-test
description
設定 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 - 如果測試無頭網頁: 使用
-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:
工作流程:端到端整合測試
複製並遵循此檢查清單來實作與驗證整合測試。
- [ ] 任務進度:設定
- [ ] 在
pubspec.yaml中加入integration_test與flutter_test。 - [ ] 在應用程式進入點注入
enableFlutterDriverExtension()。 - [ ] 為目標 widget 指定
ValueKey。
- [ ] 在
- [ ] 任務進度:探索
- [ ] 透過 MCP 執行
launch_app。 - [ ] 使用
get_widget_tree繪製 widget 樹。 - [ ] 使用 MCP 工具(
tap、enter_text)驗證互動路徑。
- [ ] 透過 MCP 執行
- [ ] 任務進度:撰寫
- [ ] 建立
integration_test/app_test.dart。 - [ ] 使用
WidgetTesterAPI 撰寫測試案例。 - [ ] 建立
test_driver/integration_test.dart,內容包含integrationDriver()。
- [ ] 建立
- [ ] 任務進度:執行與回饋迴圈
- [ ] 執行
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,
);
}
},
);
}






