Testowanie integracyjne i Flutter DevTools: kompleksowo na prawdziwym urządzeniu
Testy integracyjne to najbardziej realistyczny poziom testów Fluttera: działają dalej urządzenie fizyczne lub emulator, uruchom aplikację tak, jak zrobiłby to prawdziwy użytkownik i sprawdź kompletne przepływy od danych wejściowych interfejsu użytkownika do odpowiedzi zaplecza. W przeciwieństwie do widżetów testowych które izolują pojedynczy komponent, testy integracyjne przechodzą przez cały stos aplikacja — nawigacja, zarządzanie stanem, HTTP, pamięć lokalna.
W tym artykule budujemy kompletny potok testów integracyjnych: piszemy testy
z paczką integration_test, uruchamiamy je lokalnie na emulatorze,
automatyzujemy je w GitHub Actions i finalnie uruchamiamy na rzeczywistych urządzeniach fizycznych
z laboratorium testowym Firebase. Używamy Flutter DevTools do analizy pamięci i procesora podczas
wykonanie, identyfikowanie regresji wydajności przed wdrożeniem.
Czego się nauczysz
- Różnica między testami jednostkowymi, testami widgetów i testami integracyjnymi we Flutterze
- Konfiguracja pakietu
integration_testi testuj strukturę pliku - Pisanie testów E2E za pomocą
IntegrationTestWidgetsFlutterBinding - Użycie
find,tap,enterTextepumpAndSettle - Flutter DevTools: Profiler pamięci, Profiler procesora i zakładka Sieć
- Akcje GitHub: Potok CI/CD do testów integracyjnych na emulatorze
- Laboratorium testowe Firebase: działa na szeregu urządzeń fizycznych
- Szydercze strategie izolowania backendu w testowaniu E2E
Piramida testowa w Flutter
Przed napisaniem testów integracyjnych ważne jest, aby zrozumieć, gdzie one pasują Piramida testowania trzepotania i ich koszt/korzyść w porównaniu z innymi poziomami.
| Typ | Prędkość | Lojalność | Konserwacja | Kiedy ich używać |
|---|---|---|---|---|
| Testy jednostkowe | ~1 ms na test | Niski (izolowana logika) | Minimalny | Logika biznesowa, repozytorium, narzędzia |
| Testy widgetów | ~50 ms na test | Średni (UI bez urządzenia) | Przeciętny | Komponenty interfejsu użytkownika, interakcje |
| Próba integracji | ~30 s na test | Wysoka (prawdziwe urządzenie) | Wysoki | Przepływy krytyczne, regresja E2E |
Praktyczna zasada: 70% testów jednostkowych, 20% testów widżetów, 10% testów integracyjnych. Testy integracja jest cenna, ale droga — wykorzystaj ją do kluczowych przepływów biznesowych (logowanie, kasa, onboarding), gdzie regresja powoduje realną szkodę.
Konfiguracja 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
Pierwszy test integracyjny
Powiązanie testów integracyjnych różni się od powiązania testów widgetów:
IntegrationTestWidgetsFlutterBinding.ensureInitialized() zainicjować
powiązanie, które komunikuje się z urządzeniem natywnym i umożliwia zbieranie metryk wydajności.
// 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);
});
});
}
Testowanie pełnego przepływu uwierzytelniania
Przepływ logowania jest idealnym kandydatem do testów integracyjnych: angażuje Interfejs użytkownika, sprawdzanie poprawności, wywoływanie HTTP, przechowywanie tokenów i nawigacja po zalogowaniu.
// 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);
});
});
}
Zbiór wskaźników wydajności
Testowanie integracyjne to nie tylko poprawność funkcjonalna: wiążący test integracyjny Umożliwia gromadzenie danych dotyczących taktowania klatek, pamięci i renderowania w czasie wykonywania testu, tworząc raport JSON, który można przeanalizować w 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: Głębokie profilowanie
Flutter DevTools to zestaw narzędzi diagnostycznych dostępnych za pośrednictwem przeglądarki gdy aplikacja działa w trybie debugowania lub profilu. Najbardziej przydatne zakładki do zidentyfikowania Problemy z wydajnością to Karta Wydajność (taktowanie ramki), the Zakładka Pamięć (alokacja sterty) i Karta Sieć (opóźnienie 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
Wzór wycieku pamięci powszechny w Flutter
Najczęstszym wyciekiem pamięci w testach integracyjnych (i w produkcji) jestKontroler animacji
nie mam ochoty: Kontroler utworzony w initState bez odpowiedniego
dispose() gromadzi słuchaczy i nigdy nie jest uwalniany przez moduł zbierający elementy bezużyteczne.
Zakładka Flutter DevTools Memory identyfikuje go jako obiekt z zachowaniem ścieżki do katalogu głównego.
Działania GitHub: Potok CI/CD do testowania integracji
Uruchamianie testów integracyjnych w CI wymaga emulatora Androida lub symulatora iOS. Oto pełna konfiguracja GitHub Actions zoptymalizowana pod kątem szybkiego czasu kompilacji.
# .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/
Laboratorium testowe Firebase: macierz urządzeń fizycznych
Akcja GitHub z emulatorami obejmuje podstawowe testowanie, ale urządzenia fizyczne mają rzeczywiste różnice sprzętowe (GPU, czujniki, warianty OEM Androida). emulator nie gra. Laboratorium testowe Firebase oferuje flotę urządzeń pliki fizyczne, na których można uruchomić aplikację.
# 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
Strategia drwiąca dla niezawodnych testów E2E
Testy zależne od prawdziwego backendu są delikatne: serwer może nie działać,
dane mogą się zmieniać, opóźnienie jest różne. Najlepsza strategia testowania
integracja i wykorzystanie a wyśmiewaj serwer lokalny (Jak mockito
lub fałszywy serwer HTTP) konfigurowalny poprzez --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();
}
}
Najlepsze praktyki dotyczące stabilnych testów integracyjnych
-
Użyj jawnych kluczy: każdy interaktywny widget musi mieć
Keystała, aby można ją było znaleźć w testach w stabilny sposób. -
Ty wolisz
pumpAndSettleapump:pumpAndSettlepoczekaj na wszystkie wiszące animacje i ramki koniec, redukując niestabilność testów ze względu na czas. -
Wyraźne limity czasu: USA
pumpAndSettle(Duration(seconds: 5))dla operacji asynchronicznych zamiast domyślnego nieskończonego limitu czasu. -
Resetowanie stanu pomiędzy testami: USA
tearDowndla wyczyść SharedPreferences, lokalne bazy danych i tokeny uwierzytelniające. -
Wyłącz animacje: w rurociągach CI w USA
--no-enable-impelleri wyłącz animacje, aby przyspieszyć wykonanie o 40%.
Wnioski
Testy integracyjne są ostatnią linią obrony przed wdrożeniem na produkcję: wychwytują regresje, które wymykają się testom jednostkowym, ponieważ powstają w wyniku interakcji pomiędzy rzeczywistymi komponentami na prawdziwym sprzęcie. Koszt jest wysoki — 30–60 sekund na test, złożoność konfiguracji, bieżąca konserwacja — ale w przypadku krytycznych przepływów biznesowych ROI jest bezdyskusyjny.
Połączenie GitHub Actions umożliwiające szybką informację zwrotną na temat każdego PR i Firebase Laboratorium testowe pod kątem zasięgu heterogenicznych urządzeń fizycznych tworzy siatkę bezpieczeństwa solidna, która pozwala na pewne wdrożenie. Flutter DevTools uzupełnia obraz zapewniając wgląd w wydajność w czasie wykonywania, przekształcając testy integracja od prostych kontroli poprawności po narzędzia monitorujące jakość doświadczenia użytkownika.







