統合テストと Flutter DevTools: 実デバイス上のエンドツーエンド
統合テストは、最も現実的なレベルの Flutter テストです。 物理デバイスまたはエミュレータで、実際のユーザーが行うのと同じようにアプリを操作して検証します。 UI 入力からバックエンド応答までの完全なフロー。テストウィジェットとは異なります 単一のコンポーネントを分離する場合、統合テストはスタック全体を横断します。 アプリケーション — ナビゲーション、状態管理、HTTP、ローカル ストレージ。
この記事では、完全な統合テスト パイプラインを構築します。テストを作成します。
パッケージ付き integration_testエミュレータ上でローカルに実行します。
GitHub Actions 上でそれらを自動化し、最終的には実際の物理デバイス上で実行します。
Firebase Test Labを使用してFlutter DevTools を使用してメモリと CPU を分析します。
実行して、展開前にパフォーマンスの低下を特定します。
何を学ぶか
- Flutterにおける単体テスト、ウィジェットテスト、統合テストの違い
- パッケージのセットアップ
integration_testおよびテストファイル構造 - E2E テストを作成する
IntegrationTestWidgetsFlutterBinding - の使用
find,tap,enterTextepumpAndSettle - Flutter DevTools: メモリ プロファイラー、CPU プロファイラー、および [ネットワーク] タブ
- GitHub Actions: エミュレータでの統合テスト用の CI/CD パイプライン
- Firebase Test Lab: 物理デバイスのアレイ上で実行
- E2E テストでバックエンドを分離するためのモック戦略
Flutter のテスト ピラミッド
統合テストを作成する前に、統合テストがどこに当てはまるかを理解することが重要です フラッター テスト ピラミッドとそのコスト/利点を他のレベルと比較して説明します。
| タイプ | スピード | 忠誠心 | メンテナンス | いつ使用するか |
|---|---|---|---|---|
| 単体テスト | テストごとに最大 1 ミリ秒 | 低 (絶縁ロジック) | 最小限 | ビジネスロジック、リポジトリ、ユーティリティ |
| ウィジェットのテスト | テストごとに最大 50 ミリ秒 | 中 (デバイスなしの UI) | 平均 | UIコンポーネント、インタラクション |
| 結合テスト | テストごとに最大 30 秒 | 高 (実デバイス) | 高い | クリティカルフロー、E2E回帰 |
経験則: 70% 単体テスト、20% ウィジェット テスト、10% 統合テスト。のテスト 統合は価値がありますが高価です。重要なビジネス フロー (ログイン、 チェックアウト、オンボーディングなど)、回帰が実害を引き起こす場合。
プロジェクトのセットアップ
# pubspec.yaml: dipendenze per integration testing
dependencies:
flutter:
sdk: flutter
integration_test:
sdk: flutter # gia incluso nell'SDK Flutter
dev_dependencies:
flutter_test:
sdk: flutter
integration_test:
sdk: flutter
mocktail: ^1.0.4 # per mock degli HTTP client
fake_async: ^1.3.1 # per controllare il tempo nei test
# Struttura directory raccomandata
# test/ unit test e widget test
# integration_test/ integration test
# app_test.dart
# flows/
# auth_flow_test.dart
# checkout_flow_test.dart
# helpers/
# test_helpers.dart
最初の統合テスト
統合テストのバインディングは、ウィジェット テストのバインディングとは異なります。
IntegrationTestWidgetsFlutterBinding.ensureInitialized() 初期化する
ネイティブ デバイスと通信し、パフォーマンス メトリックの収集を可能にするバインディング。
// integration_test/app_test.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() {
// OBBLIGATORIO: inizializza il binding integration test
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('App Integration Tests', () {
testWidgets('App si avvia e mostra la home page', (tester) async {
// Avvia l'app completa (non un widget isolato)
app.main();
await tester.pumpAndSettle(); // Aspetta che tutte le animazioni finiscano
// Verifica che la home page sia visibile
expect(find.text('Benvenuto'), findsOneWidget);
expect(find.byKey(const Key('home_page')), findsOneWidget);
});
testWidgets('Navigazione tra le tab funziona', (tester) async {
app.main();
await tester.pumpAndSettle();
// Tap sulla tab Profile
await tester.tap(find.byKey(const Key('nav_profile')));
await tester.pumpAndSettle();
// Verifica che la pagina profilo sia caricata
expect(find.byKey(const Key('profile_page')), findsOneWidget);
// Torna alla Home
await tester.tap(find.byKey(const Key('nav_home')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('home_page')), findsOneWidget);
});
});
}
完全な認証フローのテスト
ログイン フローは統合テストの理想的な候補です。 UI、検証、HTTP 呼び出し、トークン ストレージ、ログイン後のナビゲーション。
// integration_test/flows/auth_flow_test.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() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Auth Flow', () {
testWidgets('Login con credenziali valide naviga alla home', (tester) async {
app.main();
await tester.pumpAndSettle();
// Trova e compila il campo email
final emailField = find.byKey(const Key('email_field'));
expect(emailField, findsOneWidget);
await tester.tap(emailField);
await tester.enterText(emailField, 'test@example.com');
// Compila il campo password
final passwordField = find.byKey(const Key('password_field'));
await tester.tap(passwordField);
await tester.enterText(passwordField, 'password123');
// Chiudi la tastiera
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
// Tap sul pulsante di login
await tester.tap(find.byKey(const Key('login_button')));
// Aspetta che il login HTTP completi (max 5 secondi)
await tester.pumpAndSettle(const Duration(seconds: 5));
// Verifica redirect alla home page
expect(find.byKey(const Key('home_page')), findsOneWidget);
expect(find.byKey(const Key('login_page')), findsNothing);
});
testWidgets('Login con credenziali errate mostra errore', (tester) async {
app.main();
await tester.pumpAndSettle();
await tester.enterText(
find.byKey(const Key('email_field')),
'wrong@example.com',
);
await tester.enterText(
find.byKey(const Key('password_field')),
'wrongpassword',
);
await tester.tap(find.byKey(const Key('login_button')));
await tester.pumpAndSettle(const Duration(seconds: 5));
// Deve apparire il messaggio di errore
expect(find.text('Credenziali non valide'), findsOneWidget);
// Deve rimanere sulla login page
expect(find.byKey(const Key('login_page')), findsOneWidget);
});
testWidgets('Validazione form: email non valida blocca il submit', (tester) async {
app.main();
await tester.pumpAndSettle();
await tester.enterText(
find.byKey(const Key('email_field')),
'email-non-valida',
);
await tester.tap(find.byKey(const Key('login_button')));
await tester.pumpAndSettle();
// Errore di validazione visibile (no HTTP call effettuata)
expect(find.text('Inserisci un\'email valida'), findsOneWidget);
});
});
}
パフォーマンスメトリクスの収集
統合テストは機能の正しさだけではありません: バインディング統合テスト 実行時にフレーム タイミング、メモリ、レンダリング メトリクスを収集できます。 テストを実行し、CI で解析できる JSON レポートを生成します。
// integration_test/performance_test.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() {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Scrolling della lista prodotti: performance test', (tester) async {
app.main();
await tester.pumpAndSettle();
// Naviga alla lista prodotti
await tester.tap(find.byKey(const Key('nav_products')));
await tester.pumpAndSettle();
// Raccoglie metriche durante il scroll
await binding.watchPerformance(() async {
// Scroll veloce per 5 paginate
for (int i = 0; i < 5; i++) {
await tester.fling(
find.byKey(const Key('products_list')),
const Offset(0, -500),
3000, // velocita pixels/secondo
);
await tester.pumpAndSettle();
}
},
reportKey: 'products_scroll_perf');
// Le metriche vengono salvate automaticamente in un file JSON
// Accessibile via: flutter drive --profile
});
}
// Comando per raccogliere metriche in modalita profile:
// flutter drive \
// --driver=test_driver/integration_test.dart \
// --target=integration_test/performance_test.dart \
// --profile
Flutter DevTools: ディーププロファイリング
Flutter DevTools はブラウザ経由でアクセスできる診断ツールのスイートです アプリがデバッグ モードまたはプロファイル モードで実行されている間。識別するのに最も役立つタブ パフォーマンスの問題は、 「パフォーマンス」タブ (フレームタイミング)、 の メモリタブ (ヒープ割り当て) 「ネットワーク」タブ (HTTP 遅延)。
# Avviare DevTools durante un integration test
# 1. Lancia l'app in modalita debug su emulatore
flutter run --debug
# 2. Apri DevTools (automaticamente o manualmente)
flutter pub global run devtools
# 3. Oppure direttamente da VS Code / Android Studio
# View > Command Palette > Flutter: Open DevTools
# Performance tab: comandi utili
# - "Record" per catturare una sessione
# - "Enhance Tracing" per shader e build details
# - Filtra per "Janky frames" (rosso = sopra 16ms budget)
# Memory tab: identificare memory leak
# - "GC" button: forza garbage collection
# - "Snapshot" prima e dopo un'operazione
# - Confronta gli heap dump per trovare oggetti non rilasciati
# Network tab
# - Mostra tutte le richieste HTTP/HTTPS
# - Timing breakdown: DNS, connect, send, wait, receive
# - Filtro per URI pattern
Flutter で一般的なメモリ リーク パターン
統合テスト (および運用環境) で最も一般的なメモリ リークは次のとおりです。アニメーションコントローラー
やる気がない: で作成されたコントローラー initState 対応するものがない場合
dispose() リスナーを蓄積し、ガベージ コレクターによって解放されることはありません。
Flutter DevTools の [メモリ] タブでは、ルートへのパスを保持するオブジェクトとして識別されます。
GitHub アクション: 統合テスト用の CI/CD パイプライン
CI で統合テストを実行するには、Android エミュレーターまたは iOS シミュレーターが必要です。 ここでは、ビルド時間を短縮するために最適化された完全な GitHub Actions セットアップを示します。
# .github/workflows/integration-tests.yml
name: Integration Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
integration-tests-android:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Java (richiesto per emulatore Android)
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.27.0'
channel: 'stable'
cache: true # cache delle dipendenze Flutter
- name: Install dependencies
run: flutter pub get
- name: Enable KVM (accelerazione hardware emulatore)
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Avvia emulatore Android
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 33
arch: x86_64
profile: Nexus 6
avd-name: integration_test_avd
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim
disable-animations: true # disabilita animazioni per test piu veloci
script: |
flutter test integration_test/ \
--flavor development \
-d emulator-5554 \
--dart-define=ENVIRONMENT=test
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: integration-test-results
path: build/integration_test_results/
Firebase Test Lab: 物理デバイス マトリックス
エミュレータを使用した GitHub Actions は基本的なテストをカバーしますが、物理デバイスもカバーします 実際のハードウェアの違い (GPU、センサー、OEM Android バリアント) があります。 エミュレータはプレイしません。 Firebase Test Lab はさまざまなデバイスを提供します アプリを実行する物理ファイル。
# Preparazione dell'APK per Firebase Test Lab
# 1. Build dell'app e del test APK separati
flutter build apk --debug --target-platform android-arm64
flutter build apk --debug \
--target=integration_test/app_test.dart \
--target-platform android-arm64
# 2. Upload e avvio del test su Firebase Test Lab via gcloud CLI
gcloud firebase test android run \
--type instrumentation \
--app build/app/outputs/apk/debug/app-debug.apk \
--test build/app/outputs/apk/debug/app-debug-androidTest.apk \
--device model=Pixel6,version=33,locale=it,orientation=portrait \
--device model=SamsungS22,version=32,locale=it,orientation=portrait \
--device model=OnePlus9,version=31,locale=it,orientation=portrait \
--timeout 5m \
--results-bucket=gs://my-project-test-results \
--results-dir=integration_tests/$(date +%Y%m%d_%H%M%S)
# Integrazione Firebase Test Lab in GitHub Actions
- name: Authenticate Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_CREDENTIALS }}
- name: Setup gcloud CLI
uses: google-github-actions/setup-gcloud@v2
- name: Build test APKs
run: |
flutter build apk --debug
flutter build apk --debug \
--target=integration_test/app_test.dart
- name: Run tests 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/debug/app-debug-androidTest.apk \
--device model=Pixel6,version=33,locale=it,orientation=portrait \
--timeout 5m \
--results-bucket=gs://my-app-test-results
信頼性の高い E2E テストのためのモッキング戦略
実際のバックエンドに依存するテストは脆弱です。サーバーがダウンする可能性があります。
データは変更される可能性があり、レイテンシも変化します。テストに最適な戦略
統合して使用します モックローカルサーバー (として mockito
または偽の HTTP サーバー) 経由で構成可能 --dart-define.
# main.dart: configurazione per ambiente test
// main.dart
void main() {
// Legge la variabile di ambiente iniettata dalla CI
const environment = String.fromEnvironment(
'ENVIRONMENT',
defaultValue: 'production',
);
if (environment == 'test') {
// Usa il mock HTTP client per i test di integrazione
HttpOverrides.global = _MockHttpOverrides();
}
runApp(
ProviderScope(
overrides: environment == 'test'
? [
// Override Riverpod provider con il mock repository
apiClientProvider.overrideWithValue(MockApiClient()),
]
: [],
child: const MyApp(),
),
);
}
class _MockHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext? context) {
// Intercetta tutte le richieste HTTP e restituisce dati fissi
return MockHttpClient();
}
}
安定した統合テストのベスト プラクティス
-
明示的キーを使用します。 すべてのインタラクティブなウィジェットには
Key安定した方法でテストで見つけられるようにするために定数を使用します。 -
あなたが好む
pumpAndSettleapump:pumpAndSettleすべてのハングアニメーションとフレームを待ちます 終了し、タイミングによる不安定なテストが減少します。 -
明示的なタイムアウト: アメリカ合衆国
pumpAndSettle(Duration(seconds: 5))非同期操作の場合、デフォルトの無限タイムアウトの代わりに。 -
テスト間の状態のリセット: アメリカ合衆国
tearDownのために クリーンな SharedPreferences、ローカル データベース、認証トークン。 -
アニメーションを無効にする: 米国の CI パイプラインで
--no-enable-impellerアニメーションを無効にして高速化します 執行率は40%。
結論
統合テストは、運用環境に展開する前の最後の防御線です。 相互作用から生じるため、単体テストを回避する回帰を捕捉します。 実際のハードウェア上の実際のコンポーネント間。コストは高く、テストごとに 30 ~ 60 秒かかります。 セットアップの複雑さ、継続的なメンテナンス - ただし重要なビジネス フローの場合 ROI には議論の余地がありません。
GitHub アクションの組み合わせにより、すべての PR と Firebase に対する素早いフィードバックが可能になります。 異種の物理デバイスをカバーするテスト ラボがセーフティ ネットを作成 堅牢なので、安心して導入できます。 Flutter DevTools が全体像を完成させます 実行時のパフォーマンスを可視化し、テストを変革します 単純な正当性チェックから監視ツールまでの統合 ユーザーエクスペリエンスの質。







