Integrační testování a flutter DevTools: End-to-End na skutečném zařízení
Integrační testy jsou nejrealističtější úrovní testování Flutter: běží dál fyzické zařízení nebo emulátor, řiďte aplikaci jako skutečný uživatel a ověřte kompletní toky od vstupu uživatelského rozhraní po odezvu backendu. Na rozdíl od testovacích widgetů které izolují jedinou komponentu, integrační testy procházejí celým zásobníkem aplikace — navigace, správa stavu, HTTP, lokální úložiště.
V tomto článku vytváříme kompletní integrační testovací kanál: píšeme testy
s balíčkem integration_test, spouštíme je lokálně na emulátoru,
automatizujeme je na GitHub Actions a nakonec je spouštíme na skutečných fyzických zařízeních
s Firebase Test Lab. K analýze paměti a CPU používáme Flutter DevTools
provedení, identifikace regresí výkonu před nasazením.
Co se naučíte
- Rozdíl mezi testy jednotek, testy widgetů a testy integrace ve Flutteru
- Nastavení balíčku
integration_testa testovací strukturu souboru - Psaní E2E testů s
IntegrationTestWidgetsFlutterBinding - Použití
find,tap,enterTextepumpAndSettle - Flutter DevTools: Memory Profiler, CPU Profiler a karta Network
- Akce GitHubu: kanál CI/CD pro integrační testy na emulátoru
- Firebase Test Lab: Běží na řadě fyzických zařízení
- Mocking strategie pro izolaci backendu v E2E testování
Zkušební pyramida ve Flutteru
Před psaním integračních testů je nezbytné pochopit, kam zapadají Flutter testovací pyramida a jaká je jejich cena/přínos ve srovnání s ostatními úrovněmi.
| Typ | Rychlost | Věrnost | Údržba | Kdy je použít |
|---|---|---|---|---|
| Jednotkové testy | ~1 ms na test | Nízká (izolovaná logika) | Minimální | Obchodní logika, úložiště, utils |
| Testy widgetů | ~50 ms na test | Střední (uživatelské rozhraní bez zařízení) | Průměrný | Komponenty uživatelského rozhraní, interakce |
| Integrační test | ~30 s na test | Vysoká (skutečné zařízení) | Vysoký | Kritické toky, E2E regrese |
Základní pravidlo: 70 % testy jednotek, 20 % testy widgetů, 10 % integrační testy. Testy z integrace jsou cenné, ale drahé – použijte je pro kritické obchodní toky (přihlášení, checkout, onboarding), kde regrese způsobí skutečnou škodu.
Nastavení projektu
# 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
První integrační test
Vazba pro integrační testy se liší od vazeb testů widgetů:
IntegrationTestWidgetsFlutterBinding.ensureInitialized() inicializovat
vazba, která komunikuje s nativním zařízením a umožňuje sběr metrik výkonu.
// 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);
});
});
}
Testování úplného toku autentizace
Přihlašovací tok je ideálním kandidátem pro testování integrace: zapojuje se Uživatelské rozhraní, ověřování, volání HTTP, úložiště tokenů a navigace po přihlášení.
// 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);
});
});
}
Sbírka metrik výkonu
Integrační testování není jen funkční správnost: závazný integrační test Umožňuje shromažďovat metriky časování snímků, paměti a vykreslování za běhu testu a vytvoří zprávu JSON, kterou lze analyzovat v CI.
// 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: Hluboké profilování
Flutter DevTools je sada diagnostických nástrojů přístupných prostřednictvím prohlížeče když je aplikace spuštěna v režimu ladění nebo profilu. Nejužitečnější karty k identifikaci problémy s výkonem jsou Karta Výkon (časování snímků), a Karta Paměť (přidělení haldy) a Karta Síť (latence 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
Vzorec úniku paměti běžný ve Flutter
Nejčastější únik paměti v integračních testech (a ve výrobě) jeAnimationController
není ochoten: Ovladač vytvořený v initState bez odpovídajícího
dispose() hromadí posluchače a popelář je nikdy nevypustí.
Karta Flutter DevTools Memory jej identifikuje jako objekt se zachováním cesty ke kořenu.
Akce GitHubu: CI/CD kanál pro testování integrace
Spuštění integračních testů v CI vyžaduje emulátor Android nebo simulátor iOS. Zde je kompletní nastavení GitHub Actions optimalizované pro rychlé sestavení.
# .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: matice fyzických zařízení
GitHub Actions with Emulators pokrývá základní testování, ale fyzická zařízení mají skutečné hardwarové rozdíly (GPU, senzory, OEM varianty Androidu), které emulátor nehraje. Firebase Test Lab nabízí flotilu zařízení fyzické soubory, na kterých se má aplikace spustit.
# 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
Strategie zesměšňování pro spolehlivé testy E2E
Testy, které závisí na skutečném backendu, jsou křehké: server může být mimo provoz,
data se mohou měnit, latence se liší. Nejlepší strategie pro testování
integrace a použití a zesměšňovat místní server (jako mockito
nebo falešný HTTP server) konfigurovatelné přes --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();
}
}
Osvědčené postupy pro testy stabilní integrace
-
Použít explicitní klíče: každý interaktivní widget musí mít a
Keykonstantní, aby bylo možné jej v testech stabilně nalézt. -
Dáváte přednost
pumpAndSettleapump:pumpAndSettlepočkejte na všechny závěsné animace a snímky konec, čímž se omezí nekvalitní testy kvůli načasování. -
Explicitní časové limity: USA
pumpAndSettle(Duration(seconds: 5))pro asynchronní operace namísto výchozího nekonečného časového limitu. -
Resetování stavu mezi testy: USA
tearDownpro vyčistit sdílené předvolby, místní databáze a ověřovací tokeny. -
Zakázat animace: v ropovodech CI v USA
--no-enable-impellera deaktivujte animace pro zrychlení provedení o 40 %.
Závěry
Integrační testy jsou poslední linií obrany před nasazením do výroby: zachycují regrese, které unikají jednotkovým testům, protože vycházejí z interakce mezi skutečnými komponenty na skutečném hardwaru. Cena je vysoká — 30–60 sekund na test, složitost nastavení, průběžná údržba – ale pro kritické obchodní toky ROI je nesporná.
Kombinace akcí GitHub pro rychlou zpětnou vazbu ke každému PR a Firebase Test Lab pro pokrytí napříč heterogenními fyzickými zařízeními vytváří záchrannou síť pevný, který vám umožní nasazení s jistotou. Flutter DevTools doplňuje obrázek poskytování viditelnosti výkonu za běhu, transformace testů integrace od jednoduchých kontrol správnosti až po monitorovací nástroje kvalitu uživatelské zkušenosti.







