Integratietesten en Flutter DevTools: end-to-end op een echt apparaat
Integratietests zijn het meest realistische niveau van Flutter-testen: ze draaien door fysiek apparaat of emulator, bestuur de app zoals een echte gebruiker dat zou doen en verifieer complete stromen van UI-invoer tot backend-reactie. In tegenstelling tot testwidgets die één enkel onderdeel isoleren, doorkruisen integratietests de hele stapel applicatie - navigatie, statusbeheer, HTTP, lokale opslag.
In dit artikel bouwen we een complete integratietestpijplijn: we schrijven de tests
met het pakket integration_test, we voeren ze lokaal uit op de emulator,
we automatiseren ze op GitHub Actions en voeren ze uiteindelijk uit op echte fysieke apparaten
met Firebase Testlab. We gebruiken Flutter DevTools om het geheugen en de CPU te analyseren tijdens
uitvoering, waarbij prestatieregressies vóór de implementatie worden geïdentificeerd.
Wat je gaat leren
- Verschil tussen unittests, widgettests en integratietests in Flutter
- Pakketconfiguratie
integration_testen testbestandsstructuur - E2E-tests schrijven met
IntegrationTestWidgetsFlutterBinding - Gebruik van
find,tap,enterTextepumpAndSettle - Flutter DevTools: tabblad Geheugenprofiler, CPU-profiler en netwerk
- GitHub-acties: CI/CD-pijplijn voor integratietests op de emulator
- Firebase Test Lab: draait op een reeks fysieke apparaten
- Spotstrategieën voor het isoleren van de backend bij E2E-testen
De testpiramide in beweging
Voordat u integratietests schrijft, is het essentieel om te begrijpen waar ze in passen Fluttertestpiramide en wat de kosten/baten ervan zijn vergeleken met de andere niveaus.
| Type | Snelheid | Loyaliteit | Onderhoud | Wanneer moet u ze gebruiken? |
|---|---|---|---|---|
| Eenheidstests | ~1 ms per test | Laag (geïsoleerde logica) | Minimaal | Bedrijfslogica, repository, gebruiksmogelijkheden |
| Widgettesten | ~50 ms per test | Medium (UI zonder apparaat) | Gemiddeld | UI-componenten, interacties |
| Integratietest | ~30s per test | Hoog (echt apparaat) | Hoog | Kritieke stromen, E2E-regressie |
De vuistregel: 70% unit-tests, 20% widget-tests, 10% integratietests. De testen van integratie zijn waardevol maar duur – gebruik ze voor kritieke bedrijfsstromen (inloggen, afrekenen, onboarding) waarbij een regressie echte schade aanricht.
Projectopstelling
# 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
De eerste integratietest
De binding voor integratietests is anders dan die voor widgettests:
IntegrationTestWidgetsFlutterBinding.ensureInitialized() initialiseren
de binding die communiceert met het native apparaat en het verzamelen van prestatiestatistieken mogelijk maakt.
// 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);
});
});
}
Een volledige authenticatiestroom testen
Een inlogstroom is de ideale kandidaat voor integratietesten: het boeit UI, validatie, HTTP-oproepen, tokenopslag en navigatie na inloggen.
// 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);
});
});
}
Verzameling van prestatiestatistieken
Integratietesten zijn niet alleen functionele correctheid: de bindende integratietest Hiermee kunt u tijdens runtime frametiming, geheugen en weergavestatistieken verzamelen van de test, waarbij een JSON-rapport wordt geproduceerd dat in CI kan worden geparseerd.
// 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: diepgaande profilering
Flutter DevTools is een reeks diagnostische hulpmiddelen die toegankelijk zijn via een browser terwijl de app in debug- of profielmodus draait. De handigste tabbladen om te identificeren Prestatieproblemen zijn de Tabblad Prestaties (frametiming), de Tabblad Geheugen (heaptoewijzing) en de Tabblad Netwerk (HTTP-latentie).
# 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
Geheugenlekpatroon dat vaak voorkomt bij flutter
Het meest voorkomende geheugenlek bij integratietests (en in productie) isAnimatieController
niet bereid: Een controller gemaakt in initState zonder de bijbehorende
dispose() verzamelt luisteraars en wordt nooit vrijgegeven door de vuilnisman.
Het tabblad Flutter DevTools Memory identificeert het als een object met een vasthoudpad naar de root.
GitHub-acties: CI/CD-pijplijn voor integratietesten
Voor het uitvoeren van integratietests in CI is een Android-emulator of een iOS-simulator vereist. Hier is een complete GitHub Actions-installatie, geoptimaliseerd voor snelle bouwtijden.
# .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-testlab: fysieke apparaatmatrix
GitHub-acties met emulators omvatten basistesten, maar ook fysieke apparaten ze hebben echte hardwareverschillen (GPU, sensoren, OEM Android-varianten). de emulator speelt niet. Firebase Test Lab biedt een reeks apparaten fysieke bestanden waarop de app moet worden uitgevoerd.
# 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
Spotstrategie voor betrouwbare E2E-tests
Tests die afhankelijk zijn van een echte backend zijn kwetsbaar: de server kan uitvallen,
gegevens kunnen veranderen, de latentie varieert. De beste strategie om te testen
integratie en gebruik a nep-lokale server (als mockito
of een nep-HTTP-server) configureerbaar via --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();
}
}
Best practices voor stabiele integratietests
-
Gebruik expliciete sleutels: elke interactieve widget moet een
Keyconstant om het op een stabiele manier vindbaar te maken in tests. -
Jij geeft de voorkeur
pumpAndSettleapump:pumpAndSettlewacht op alle hangende animaties en frames einde, waardoor vlokkige tests als gevolg van timing worden verminderd. -
Expliciete time-outs: VS
pumpAndSettle(Duration(seconds: 5))voor asynchrone bewerkingen in plaats van de standaard oneindige time-out. -
Status tussen tests resetten: VS
tearDownvoor schone SharedPreferences, lokale databases en authenticatietokens. -
Animaties uitschakelen: in Amerikaanse CI-pijpleidingen
--no-enable-impelleren schakel animaties uit om te versnellen uitvoering met 40%.
Conclusies
Integratietests vormen de laatste verdedigingslinie vóór implementatie in productie: ze leggen regressies vast die aan eenheidstests ontsnappen omdat ze voortkomen uit interactie tussen echte componenten op echte hardware. De kosten zijn hoog: 30-60 seconden per test, complexiteit van de installatie, doorlopend onderhoud, maar dan voor kritieke bedrijfsstromen de ROI staat buiten kijf.
De combinatie van GitHub Actions voor snelle feedback op elke PR en Firebase Testlab voor dekking over heterogene fysieke apparaten creëert een vangnet solide waarmee u met vertrouwen kunt inzetten. Flutter DevTools maakt het plaatje compleet het bieden van inzicht in de prestaties tijdens runtime, waardoor tests worden getransformeerd integratie van eenvoudige correctheidscontroles tot monitoringtools kwaliteit van de gebruikerservaring.







