Testarea integrării și Flutter DevTools: end-to-end pe dispozitiv real
Testele de integrare sunt cel mai realist nivel de testare Flutter: rulează mai departe dispozitiv fizic sau emulator, conduceți aplicația ca un utilizator real și verificați fluxuri complete de la intrarea UI la răspunsul backend. Spre deosebire de widget-urile de testare care izolează o singură componentă, testele de integrare traversează întreaga stivă aplicație — navigare, management de stat, HTTP, stocare locală.
În acest articol construim un pipeline complet de teste de integrare: scriem testele
cu pachetul integration_test, le rulăm local pe emulator,
le automatizăm pe GitHub Actions și în cele din urmă le rulăm pe dispozitive fizice reale
cu Firebase Test Lab. Folosim Flutter DevTools pentru a analiza memoria și procesorul în timpul
execuție, identificând regresiile de performanță înainte de implementare.
Ce vei învăța
- Diferența dintre testele unitare, testele widget și testele de integrare în Flutter
- Configurarea pachetului
integration_testși structura fișierului de testare - Scrierea de teste E2E cu
IntegrationTestWidgetsFlutterBinding - Utilizarea
find,tap,enterTextepumpAndSettle - Flutter DevTools: Memory Profiler, CPU Profiler și fila Network
- Acțiuni GitHub: conductă CI/CD pentru teste de integrare pe emulator
- Firebase Test Lab: rulează pe o serie de dispozitive fizice
- Strategii de batjocură pentru izolarea backend-ului în testarea E2E
Piramida de testare în Flutter
Înainte de a scrie teste de integrare, este esențial să înțelegeți unde se potrivesc Piramida de testare flutter și care este costul/beneficiul lor în comparație cu celelalte niveluri.
| Tip | Viteză | Loialitate | Întreţinere | Când să le folosești |
|---|---|---|---|---|
| Teste unitare | ~1 ms per test | Scăzut (logică izolată) | Minim | Logica de afaceri, depozit, utilitare |
| Teste widget | ~50 ms per test | Medie (UI fără dispozitiv) | Medie | Componente UI, interacțiuni |
| Test de integrare | ~30s per test | Ridicat (dispozitiv real) | Ridicat | Fluxuri critice, regresie E2E |
Regula generală: 70% teste unitare, 20% teste widget, 10% teste de integrare. Testele de integrarea sunt valoroase, dar costisitoare — utilizați-le pentru fluxuri critice de afaceri (autentificare, checkout, onboarding) unde o regresie provoacă un rău real.
Configurarea proiectului
# 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
Primul test de integrare
Legarea pentru testele de integrare este diferită de cea a testelor widget:
IntegrationTestWidgetsFlutterBinding.ensureInitialized() inițializați
legarea care comunică cu dispozitivul nativ și permite colectarea de valori de performanță.
// 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);
});
});
}
Testarea unui flux complet de autentificare
Un flux de conectare este candidatul ideal pentru testarea integrării: se angajează UI, validare, apelare HTTP, stocare token și navigare după conectare.
// 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);
});
});
}
Colecție de valori de performanță
Testarea de integrare nu este doar corectitudine funcțională: testul de integrare obligatorie Vă permite să colectați parametrii de sincronizare a cadrelor, memorie și randare în timpul rulării a testului, producând un raport JSON care poate fi analizat în 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: Deep Profiling
Flutter DevTools este o suită de instrumente de diagnosticare accesibilă prin browser în timp ce aplicația rulează în modul de depanare sau profil. Cele mai utile file de identificat problemele de performanță sunt Fila Performanță (sincronizarea cadrului), cel Fila de memorie (alocarea heap) și fila Rețea (latență 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
Model de scurgeri de memorie comun în Flutter
Cea mai frecventă pierdere de memorie în testele de integrare (și în producție) esteAnimationController
nevrând: Un controler creat în initState fără cea corespunzătoare
dispose() acumulează ascultători și nu este niciodată eliberat de gunoiul.
Fila de memorie Flutter DevTools îl identifică ca un obiect cu reținerea căii către rădăcină.
Acțiuni GitHub: conductă CI/CD pentru testarea integrării
Rularea testelor de integrare în CI necesită un emulator Android sau un simulator iOS. Iată o configurare completă GitHub Actions, optimizată pentru timpi de construcție rapidi.
# .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/
Laborator de testare Firebase: Matricea dispozitivelor fizice
GitHub Actions with Emulators acoperă testarea de bază, dar dispozitivele fizice au diferențe reale de hardware (GPU, senzori, variante OEM Android) care emulatorul nu joacă. Firebase Test Lab oferă o flotă de dispozitive fișiere fizice pe care să rulați aplicația.
# 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 de batjocură pentru teste E2E fiabile
Testele care depind de un backend real sunt fragile: serverul poate fi oprit,
datele se pot schimba, latența variază. Cea mai bună strategie de testare
integrare și utilizare a server local simulat (ca mockito
sau un server HTTP fals) configurabil prin --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();
}
}
Cele mai bune practici pentru teste de integrare stabilă
-
Utilizați chei explicite: fiecare widget interactiv trebuie să aibă un
Keyconstantă pentru a-l face găsibil în teste într-un mod stabil. -
Tu preferi
pumpAndSettleapump:pumpAndSettleașteptați toate animațiile și cadrele suspendate sfârşitul, reducând testele de fulgi datorită cronometrarii. -
Timeout-uri explicite: STATELE UNITE ALE AMERICII
pumpAndSettle(Duration(seconds: 5))pentru operații asincrone în loc de timeout-ul infinit implicit. -
Resetarea stării între teste: STATELE UNITE ALE AMERICII
tearDownpentru curățați SharedPreferences, baze de date locale și jetoane de autentificare. -
Dezactivați animațiile: în conductele CI din SUA
--no-enable-impellerși dezactivați animațiile pentru a accelera executie cu 40%.
Concluzii
Testele de integrare sunt ultima linie de apărare înainte de implementarea în producție: ele surprind regresii care scapă de testele unitare pentru că ies din interacțiune între componente reale pe hardware real. Costul este mare - 30-60 de secunde per test, complexitate de configurare, întreținere continuă - dar pentru fluxuri de afaceri critice rentabilitatea investiției este incontestabilă.
Combinația de acțiuni GitHub pentru feedback rapid cu privire la fiecare PR și Firebase Laboratorul de testare pentru acoperirea dispozitivelor fizice eterogene creează o plasă de siguranță solid care vă permite să vă desfășurați cu încredere. Flutter DevTools completează imaginea oferind vizibilitate asupra performanței în timpul execuției, transformând testele integrare de la simple verificări de corectitudine la instrumente de monitorizare calitatea experienței utilizatorului.







