통합 테스트 및 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의 테스트 피라미드
통합 테스트를 작성하기 전에 그것이 어디에 적합한지 이해하는 것이 중요합니다. Flutter 테스트 피라미드와 그 비용/이점을 다른 수준과 비교합니다.
| 유형 | 속도 | 충의 | 유지 | 언제 사용하나요? |
|---|---|---|---|---|
| 단위 테스트 | 테스트당 ~1ms | 낮음(절연 논리) | 최소한의 | 비즈니스 로직, 저장소, 유틸리티 |
| 위젯 테스트 | 테스트당 ~50ms | 중간(디바이스 없는 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 Actions: 통합 테스트를 위한 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 테스트 랩: 물리적 기기 매트릭스
에뮬레이터를 사용한 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는 확실합니다.
모든 PR 및 Firebase에 대한 빠른 피드백을 위한 GitHub Actions의 조합 이기종 물리적 장치를 포괄하는 Test Lab은 안전망을 만듭니다. 자신있게 배포할 수 있는 견고한 제품입니다. Flutter DevTools가 그림을 완성합니다 런타임 시 성능에 대한 가시성을 제공하고 테스트를 혁신합니다. 간단한 정확성 검사부터 모니터링 도구까지 통합 사용자 경험의 품질.







