Back to Blog
FlutterFirebase Test LabAutomated Testing

Chạy Flutter mobile automation tests trên Firebase Test Lab

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.

flutter_mobile_firebase_test_lab.png

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:

  • Unit tests (package test) — logic Dart thuần túy, không có UI, chạy cực nhanh.
  • Widget tests (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 tests (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ó:

  • Một Flutter app build và chạy được trên cả hai platform.
  • Flutter SDK đã cài đặt và có trong PATH.
  • Một Firebase project (tạo tại Firebase console nếu chưa có).
  • Google Cloud CLI (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.
  • Với iOS build: máy macOS có Xcode và CocoaPods.
  • 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/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:

  • Dùng 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 đó.
  • Ưu tiên tìm widget bằng key (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.
  • Giữ mỗi 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:

  • Các test dùng chung một app session và chạy tuần tự. Nếu một test để app ở trạng thái kỳ lạ, nó có thể ảnh hưởng đến test tiếp theo. Hãy reset state ở đầu mỗi test — navigate về home, xóa bất kỳ session đã đăng nhập — thay vì giả định slate sạch.
  • Đặt tên group rõ ràng. Khi một bundled run fail, tên group và test rõ ràng sẽ giúp bạn biết ngay file nào bị lỗi. Log và video vẫn chỉ ra các lỗi cụ thể, nhưng tên tốt giúp bạn tiết kiệm thời gian tìm kiếm.
  • 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:

  • Một service account. Tạo một cái trong Google Cloud với các role Firebase Test Lab Admin* và *Editor (hoặc scope hẹp hơn), tải xuống JSON key của nó, và lưu vào repository secret GCP_SA_KEY. Action auth sẽ dùng nó thay vì đăng nhập tương tác.
  • Billing. Việc sử dụng Test Lab được tính phí theo mét, vì vậy project đằng sau service account đó cần có tài khoản billing đang hoạt động.
  • 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 đề.
  • Native dialog không thấy được trong 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).
  • Device matrix tốn tiền nhanh. Mỗi device-minute đều bị tính phí. Bắt đầu với một hoặc hai device đại diện trong CI, dành device matrix rộng cho các nightly run hoặc run trước release.
  • Giữ test deterministic. Các test phụ thuộc vào live network call sẽ flaky trên thiết bị remote có độ trễ khác nhau. Mock network layer của bạn hoặc trỏ đến một staging backend ổn định.
  • Chú ý timeout. Flag --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 : )

    Enjoyed this read?

    1likes

    Found this helpful?

    Support my work.

    If this post saved you time or taught you something, you can buy me a coffee. Every bit keeps me writing and shipping.

    Support my work.

    If this post saved you time or taught you something, you can buy me a coffee. Every bit keeps me writing and shipping.

    TP BankVietnam

    Scan with any VN banking app

    Show QRHide QR
    Scan to support via Vietnamese bank (VietQR)Scan to pay
    PayPalGlobal

    Pay with your PayPal account

    Support via PayPal
    Show QRHide QR
    Scan to support via PayPalScan to pay
    GitHubGlobal

    Visa / Mastercard · one-time or monthly

    Sponsor on GitHub
    Show QRHide QR
    Scan to sponsor on GitHubScan to pay
    Comments

    Loading…

    END OF SHEET — LET'S BUILD

    Got a hard problem? Let's talk.