Viết test là một chuyện. Chạy chúng trên thiết bị thật — hàng chục loại điện thoại, phiên bản OS, và kích thước màn hình khác nhau — lại là một vấn đề hoàn toàn khác. Bạn chắc chắn không có cả tủ Pixel và iPhone, và emulator chỉ giải quyết được một phần.
Firebase Test Lab giải quyết điều đó. Đây là một cloud gồm các thiết bị thật, vật lý (và Android virtual devices) đặt trong data center của Google, và nó sẽ chạy Flutter integration tests của bạn trên tất cả chúng một cách dễ dàng. Bài viết này sẽ đi qua toàn bộ pipeline: thiết lập integration_test, viết test, chạy thử locally, đưa lên Test Lab cho cả Android và iOS, và cuối cùng kết nối vào CI để chạy tự động trên mỗi pull request.
Lưu ý về phiên bản: Flutter, gcloud CLI, và Xcode liên tục thay đổi, và các build command cụ thể đôi khi thay đổi giữa các major release. Các bước dưới đây phản ánh cách tiếp cận có tài liệu từ lâu, nhưng nếu một lệnh nào đó hoạt động không như mong đợi, hãy đối chiếu với tài liệu Flutter và Firebase hiện tại cho phiên bản tool của bạn.

Mục lục
0. Tại sao dùng integration_test + Firebase Test Lab
1. Thêm integration_test vào project
2. Viết integration test đầu tiên
3. Tổ chức nhiều test vào một entrypoint duy nhất
4. Chạy thử locally trước
5. Chạy trên Firebase Test Lab: Android
6. Chạy trên Firebase Test Lab: iOS
7. Tự động hóa trong CI/CD
8. Những lưu ý và mẹo hay
Tại sao dùng integration_test + Firebase Test Lab
Flutter có mô hình testing ba tầng:
test) — logic Dart thuần túy, không có UI, chạy cực nhanh.flutter_test) — từng widget riêng lẻ được render trên headless engine, vẫn không cần thiết bị thật.integration_test) — toàn bộ app chạy trên thiết bị thật hoặc emulator, end to end.Hai loại đầu không rời khỏi máy của bạn. Integration tests mới thực sự được hưởng lợi từ phần cứng thật, và package integration_test chính thức được thiết kế đặc biệt để hoạt động với Firebase Test Lab. Trên Android, nó đóng gói test của bạn thành một Espresso instrumentation test; trên iOS, nó tạo ra một XCTest bundle. Điều đó có nghĩa là một Dart test suite duy nhất chạy native trên cả hai platform xuyên suốt device matrix của Test Lab. Đó chính xác là sự kết hợp chúng ta sẽ xây dựng.
Prerequisites
Trước khi bắt đầu, hãy đảm bảo bạn có:
PATH.gcloud) đã cài đặt và xác thực. Test Lab được tính phí qua Google Cloud, vì vậy project cần bật billing.Xác thực và trỏ gcloud vào project của bạn một lần:
gcloud auth login
gcloud config set project YOUR_FIREBASE_PROJECT_ID
Bước 1 — Thêm integration_test vào project
Thêm package như một dev dependency. Package này đi kèm với Flutter SDK, vì vậy bạn pin nó vào SDK thay vì một số phiên bản cụ thể:
## pubspec.yaml
dev_dependencies:
integration_test:
sdk: flutter
flutter_test:
sdk: flutter
Sau đó fetch packages:
flutter pub get
Tạo một thư mục integration_test/ ở root của project (ngang hàng với lib/ và test/). Đây là nơi các end-to-end tests của bạn sẽ nằm.
Bước 2 — Viết integration test đầu tiên
Integration tests trông khá giống widget tests, nhưng chúng gọi IntegrationTestWidgetsFlutterBinding.ensureInitialized() và điều khiển app thật. Dưới đây là một ví dụ đơn giản: khởi chạy app, tìm một counter, nhấn button, và kiểm tra kết quả:
// 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' as app;
void main() {
// Required: binds the test framework to the live app on the device.
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('end-to-end flow', () {
testWidgets('increments the counter', (tester) async {
// Launch the real app.
app.main();
await tester.pumpAndSettle();
// Verify the starting state.
expect(find.text('0'), findsOneWidget);
// Interact with the UI.
final fab = find.byType(FloatingActionButton);
await tester.tap(fab);
await tester.pumpAndSettle();
// Verify the result.
expect(find.text('1'), findsOneWidget);
});
});
}
Một vài mẹo để viết test tốt:
pumpAndSettle() sau các thao tác để cho phép animation và async hoàn tất, nhưng lưu ý nó có thể bị treo với infinite animation (như một loading spinner) — chuyển sang dùng pump(Duration(...)) trong những trường hợp đó.find.byKey(const Key('login_button'))) thay vì bằng text. Key sẽ không bị ảnh hưởng bởi thay đổi nội dung hoặc localization.testWidgets block tập trung vào một user journey. Test monolithic dài rất khó debug khi chúng fail trên thiết bị remote.Bước 3 — Tổ chức nhiều test vào một entrypoint duy nhất
Đây là một chi tiết hay khiến mọi người ngạc nhiên: Gradle và Xcode build đúng một Dart entrypoint vào test artifact. Flag -Ptarget=... mà bạn sẽ thấy trong các build command bên dưới trỏ đến một file duy nhất. Vì vậy nếu bạn giữ ba file test riêng biệt và coi mỗi file là entrypoint riêng, bạn sẽ phải build và gửi lên Test Lab ba lần riêng biệt — và mỗi lần gửi sẽ khởi động thiết bị, cài đặt app, và tear down, nhân lên cloud minutes của bạn.
Bạn hầu như không bao giờ muốn điều đó. Pattern chuẩn là giữ các test tách biệt theo file để dễ đọc, sau đó tổng hợp chúng vào một entrypoint duy nhất để build. Tổ chức thư mục như sau:
integration_test/
login_test.dart
checkout_test.dart
profile_test.dart
all_tests.dart <- the aggregator you build against
Mỗi file riêng lẻ được viết bình thường — main() của nó đăng ký các testWidgets block của nó. File aggregator đơn giản chỉ gọi từng cái:
// integration_test/all_tests.dart
import 'package:integration_test/integration_test.dart';
import 'login_test.dart' as login;
import 'checkout_test.dart' as checkout;
import 'profile_test.dart' as profile;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
login.main();
checkout.main();
profile.main();
}
Cách này hoạt động vì gọi mỗi main() chỉ đơn giản là đăng ký các test group của nó vào cùng một run. (Mỗi file riêng lẻ cũng có thể tự gọi ensureInitialized() — gọi nhiều lần cũng không sao.) Sau đó bạn build dựa vào all_tests.dart một lần, và mọi test đều chạy trong một Test Lab run duy nhất.
Hai điều cần lưu ý với cách tiếp cận bundle:
Nếu một suite thực sự cần isolation — chẳng hạn nó yêu cầu cấu hình app hoàn toàn khác — thì một build riêng biệt và Test Lab run riêng là lựa chọn đúng đắn. Còn lại thì bundle tất cả vào all_tests.dart.
Bước 4 — Chạy thử locally trước
Luôn luôn xác nhận test pass trên thiết bị kết nối hoặc emulator trước khi trả tiền cho cloud minutes:
flutter test integration_test/app_test.dart
Nếu kết quả xanh, bạn đã sẵn sàng đưa lên cloud.
Bước 5 — Chạy trên Firebase Test Lab: Android
Test Lab chạy Android tests dưới dạng instrumentation tests, vì vậy bạn cần hai APK: bản thân app và test harness. Build cả hai từ thư mục android/ bằng Gradle:
pushd android
## Build the instrumentation (test) APK.
./gradlew app:assembleAndroidTest
## Build the debug app APK, pointing at your aggregated test entrypoint.
./gradlew app:assembleDebug -Ptarget=integration_test/all_tests.dart
popd
Lệnh này tạo ra hai file:
build/app/outputs/apk/debug/app-debug.apk — app của bạn.build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk — test harness.Bây giờ đưa chúng cho Test Lab. Bạn chỉ định device bằng flag --device, và có thể truyền nhiều flag để chạy trên một device matrix:
gcloud firebase test android run \
--type instrumentation \
--app build/app/outputs/apk/debug/app-debug.apk \
--test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk \
--device model=MediumPhone.arm,version=33,locale=en,orientation=portrait \
--device model=redfin,version=30,locale=en,orientation=portrait \
--timeout 5m
Bạn có thể liệt kê các device model và OS version hiện có bằng:
gcloud firebase test android models list
Khi run hoàn tất, CLI in ra một link đến kết quả trong Firebase console — log, bản ghi video, ảnh chụp màn hình, và pass/fail theo từng device.
Bước 6 — Chạy trên Firebase Test Lab: iOS
iOS phức tạp hơn một chút vì bạn build qua Xcode và tự đóng gói kết quả. Bước này yêu cầu macOS.
Đầu tiên, tạo Flutter build configuration trỏ đến test target của bạn:
flutter build ios --config-only integration_test/all_tests.dart
Sau đó build for testing với xcodebuild từ thư mục ios/. Lệnh này compile app và test bundle mà không chạy chúng:
pushd ios
xcodebuild build-for-testing \
-workspace Runner.xcworkspace \
-scheme Runner \
-xcconfig Flutter/Release.xcconfig \
-configuration Release \
-derivedDataPath ../build/ios_integ \
-sdk iphoneos
popd
Build output nằm trong build/ios_integ/Build/Products. Test Lab cần một file zip chứa thư mục product Release-iphoneos đã build và file .xctestrun được tạo ra:
pushd build/ios_integ/Build/Products
zip -r ios_tests.zip Release-iphoneos *.xctestrun
popd
Cuối cùng, gửi file zip lên Test Lab:
gcloud firebase test ios run \
--test build/ios_integ/Build/Products/ios_tests.zip \
--device model=iphone14pro,version=16.6,locale=en_US,orientation=portrait \
--timeout 5m
Tương tự Android, bạn có thể liệt kê các iOS device có sẵn:
gcloud firebase test ios models list
Bước 7 — Tự động hóa trong CI/CD
Chạy các lệnh này thủ công ổn cho lần đầu thử, nhưng lợi ích thực sự là chạy chúng tự động trên mỗi pull request. Dưới đây là một GitHub Actions workflow build Android artifacts và gửi chúng lên Test Lab. (iOS sẽ là một parallel job trên runner macos-latest theo các bước build ở trên.)
## .github/workflows/test-lab.yml
name: Firebase Test Lab
on:
pull_request:
branches: [main]
jobs:
android-test-lab:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
- run: flutter pub get
- name: Build app and test APKs
run: |
pushd android
./gradlew app:assembleAndroidTest
./gradlew app:assembleDebug -Ptarget=integration_test/all_tests.dart
popd
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
- uses: google-github-actions/setup-gcloud@v2
- name: Run on Firebase Test Lab
run: |
gcloud firebase test android run \
--type instrumentation \
--app build/app/outputs/apk/debug/app-debug.apk \
--test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk \
--device model=MediumPhone.arm,version=33 \
--timeout 5m
Hai thứ làm cho workflow này hoạt động:
GCP_SA_KEY. Action auth sẽ dùng nó thay vì đăng nhập tương tác.Những lưu ý và mẹo hay
Một số điều thường khiến mọi người vấp phải:
pumpAndSettle bị treo (flaky). Nếu một test bị treo, nguyên nhân thường là một infinite animation. Thay pumpAndSettle() bằng các lệnh pump(Duration(milliseconds: 500)) tường minh xung quanh widget gây vấn đề.integration_test.* Package này chỉ hiểu Flutter widget layer. Nếu test của bạn cần nhấn vào OS permission prompt, system share sheet, hay native WebView, integration_test thuần túy không thể với tới đó. Hãy dùng *Patrol, một framework xây dựng trên integration_test bổ sung chính xác khả năng native interaction này (và cũng hoạt động với Test Lab).--timeout sẽ kill toàn bộ run; đảm bảo nó vượt quá thời gian của test chậm nhất một cách thoải mái, hoặc bạn sẽ nhận được false failure.Tổng kết
Pattern giống nhau trên cả hai platform: viết một suite Dart integration tests, build các platform-specific test artifacts, và để gcloud firebase test chạy chúng trên phần cứng thật trên cloud. Chi phí đầu tư chủ yếu ở bước đầu — khi các build command và service account đã được kết nối vào CI, mỗi pull request sẽ được validate trên các thiết bị thật mà không cần thao tác thủ công.
Nếu app của bạn nằm trong Flutter layer, integration_test cộng với Test Lab là tất cả những gì bạn cần. Ngay khi bạn cần tương tác với native system UI, thêm Patrol vào và giữ nguyên pipeline Test Lab. Dù theo cách nào, bạn đi từ "chạy được trên máy tôi" đến "đã xác minh trên điện thoại thật" — đó chính xác là sự tự tin bạn muốn có trước khi ship.
Happy Coding : )
Loading…