Flutter 튜토리얼 46편: 통합 테스트
요약
핵심 요지
- 문제 정의: 단위 테스트와 위젯 테스트는 개별 컴포넌트만 검증한다. 전체 앱이 실제 기기에서 어떻게 동작하는지는 확인할 수 없다.
- 핵심 주장: 통합 테스트는 실제 기기나 에뮬레이터에서 앱 전체 워크플로우를 검증하고 성능을 측정한다.
- 주요 근거:
integration_test1 패키지는 위젯 테스트와 유사한 API를 제공하면서 실제 기기에서 실행된다. - 실무 기준: CI/CD 파이프라인에 통합 테스트를 포함하고, Firebase Test Lab으로 여러 기기에서 자동화된 테스트를 실행한다.
- 한계: 웹에서는 성능 타임라인 기록이 지원되지 않는다.
문서가 설명하는 범위
- 통합 테스트의 개념과 용어
- integration_test 패키지 설정
- 통합 테스트 작성과 실행
- 성능 프로파일링과 타임라인 측정
- Firebase Test Lab 연동
읽는 시간: 20분 | 난이도: 중급
참고 자료
- Integration testing concepts - 통합 테스트 개념 소개
- Write and run an integration test - 통합 테스트 작성 및 실행 가이드
- Measure performance with an integration test - 성능 프로파일링 가이드
문제 상황
이전 튜토리얼에서 단위 테스트와 위젯 테스트를 배웠습니다. 하지만 이 테스트들은 개별 컴포넌트만 검증합니다.
기존 테스트의 한계
// 단위 테스트: 함수 로직만 검증test('counter increments', () { expect(counter.value, 0); counter.increment(); expect(counter.value, 1);});
// 위젯 테스트: 단일 위젯만 검증testWidgets('shows counter value', (tester) async { await tester.pumpWidget(CounterWidget()); expect(find.text('0'), findsOneWidget);});
// 문제: 실제 앱에서 어떻게 동작하는지 모름// - 화면 전환이 제대로 되는가?// - 네트워크 요청과 UI가 연동되는가?// - 실제 기기에서 성능은 어떤가?실제 앱은 여러 화면, 네트워크 요청, 데이터베이스 등이 복합적으로 작동합니다. 이를 검증하려면 통합 테스트가 필요합니다.
해결 방법
통합 테스트는 앱 전체를 실제 기기에서 실행하고 사용자 워크플로우를 검증합니다.
챕터 1: 통합 테스트 기본 개념
Why
NOTE통합 테스트는 E2E(End-to-End) 테스트 또는 GUI 테스트라고도 합니다. 실제 사용자가 앱을 사용하는 것처럼 전체 워크플로우를 테스트합니다.
용어 정리:
- 호스트 머신: 개발 중인 컴퓨터 (데스크톱)
- 타겟 디바이스: 앱이 실행되는 기기 (폰, 에뮬레이터, 브라우저)
웹이나 데스크톱 앱의 경우, 호스트 머신과 타겟 디바이스가 같을 수 있습니다.
What
NOTE
integration_test패키지는 Flutter SDK에 포함되어 있습니다. 기존flutter_driver2를 대체하는 최신 방식입니다.integration_test의 장점:
flutter_testAPI 사용 (위젯 테스트와 유사한 문법)flutter test명령어로 실행 가능- Firebase Test Lab 지원
- 실제 기기에서 실행
How
TIP1단계: 패키지 추가
Terminal window flutter pub add 'dev:integration_test:{"sdk":"flutter"}'
pubspec.yaml에 다음이 추가됩니다.dev_dependencies:flutter_test:sdk: flutterintegration_test:sdk: flutter2단계: 디렉토리 구조 생성
counter_app/lib/main.dartintegration_test/ # 통합 테스트 디렉토리app_test.dart # 테스트 파일test/unit_test.dart # 단위/위젯 테스트
integration_test디렉토리를 프로젝트 루트에 생성합니다.
Watch out
WARNINGflutter_driver 마이그레이션: 기존
flutter_driver를 사용하던 프로젝트는 마이그레이션이 필요합니다. Migrating from flutter_driver 가이드를 참고하세요.
챕터 2: 통합 테스트 작성하기
Why
NOTE통합 테스트는 위젯 테스트와 문법이 거의 같습니다.
WidgetTester를 사용해서 탭, 스크롤, 텍스트 입력 등을 시뮬레이션합니다.차이점은 실제 기기에서 실행된다는 것입니다.
What
NOTE통합 테스트 파일의 기본 구조입니다.
import 'package:flutter/material.dart';import 'package:flutter_test/flutter_test.dart';import 'package:integration_test/integration_test.dart';import 'package:counter_app/main.dart';void main() {// 통합 테스트 바인딩 초기화 (필수!)IntegrationTestWidgetsFlutterBinding.ensureInitialized();group('end-to-end test', () {testWidgets('tap on FAB, verify counter', (tester) async {// 앱 로드await tester.pumpWidget(const MyApp());// 초기 상태 검증expect(find.text('0'), findsOneWidget);// FAB 찾기 (Key 사용)final fab = find.byKey(const ValueKey('increment'));// 탭 시뮬레이션await tester.tap(fab);// 프레임 갱신 대기await tester.pumpAndSettle();// 결과 검증expect(find.text('1'), findsOneWidget);});});}핵심 포인트:
IntegrationTestWidgetsFlutterBinding.ensureInitialized()호출 필수pumpWidget()으로 앱 로드pumpAndSettle()로 모든 애니메이션 완료 대기
How
TIP테스트 대상 앱에
Key를 추가하면 요소를 쉽게 찾을 수 있습니다.lib/main.dart floatingActionButton: FloatingActionButton(key: const Key('increment'), // 테스트에서 사용할 키onPressed: _incrementCounter,tooltip: 'Increment',child: const Icon(Icons.add),),테스트에서
Key로 요소를 찾습니다.final fab = find.byKey(const ValueKey('increment'));await tester.tap(fab);다양한 찾기 방법:
// 텍스트로 찾기find.text('Submit');// 아이콘으로 찾기find.byIcon(Icons.add);// 타입으로 찾기find.byType(FloatingActionButton);// Semantics 라벨로 찾기find.bySemanticsLabel('Increment');
Watch out
WARNINGKey 충돌 주의: 같은 화면에 동일한 Key가 있으면 테스트가 실패합니다.
// 잘못된 예: 같은 Key 사용ListView.builder(itemBuilder: (context, index) {return ListTile(key: const Key('item'), // 모든 아이템에 같은 Key!);},);// 올바른 예: 고유한 Key 사용ListView.builder(itemBuilder: (context, index) {return ListTile(key: Key('item_$index'), // 인덱스로 구분);},);
챕터 3: 통합 테스트 실행하기
Why
NOTE통합 테스트는 다양한 플랫폼에서 실행할 수 있습니다.
- 데스크톱 (macOS, Windows, Linux)
- 모바일 (Android, iOS)
- 웹 브라우저
- Firebase Test Lab (클라우드)
What
NOTE기본 실행 명령어입니다.
Terminal window flutter test integration_test/app_test.dart여러 기기가 연결된 경우 선택 프롬프트가 표시됩니다.
Connected devices:macOS (desktop) • macos • darwin-arm64iPhone 15 (simulator) • ios[1]: macOS (macos)[2]: iPhone 15 (ios)Please choose one (or "q" to quit): 1
How
TIP플랫폼별 실행 방법
데스크톱/모바일:
Terminal window # 기본 실행flutter test integration_test/app_test.dart# 특정 디바이스 지정flutter test integration_test/app_test.dart -d iosflutter test integration_test/app_test.dart -d android웹 브라우저:
ChromeDriver가 필요합니다.
Terminal window # ChromeDriver 설치npx @puppeteer/browsers install chromedriver@stable# ChromeDriver 실행 (별도 터미널)chromedriver --port=4444# 테스트 드라이버 파일 생성: test_driver/integration_test.darttest_driver/integration_test.dart import 'package:integration_test/integration_test_driver.dart';Future<void> main() => integrationDriver();Terminal window # 웹 테스트 실행flutter drive \--driver=test_driver/integration_test.dart \--target=integration_test/app_test.dart \-d chrome헤드리스 모드 (CI 환경):
Terminal window flutter drive \--driver=test_driver/integration_test.dart \--target=integration_test/app_test.dart \-d web-server
Watch out
WARNING테스트 후 앱 정리: 모바일에서 테스트가 끝나면 앱이 자동으로 삭제되지 않을 수 있습니다. 수동으로 제거하지 않으면 다음 테스트가 실패할 수 있습니다.
챕터 4: 성능 프로파일링
Why
NOTE60fps를 유지하려면 각 프레임이 16ms 안에 렌더링되어야 합니다. 성능 프로파일링은 프레임 드랍이 발생하는 구간을 찾아줍니다.
jank(버벅거림): 프레임이 16ms를 초과해서 화면이 끊기는 현상
What
NOTE
traceAction()3 메서드로 특정 작업의 성능을 기록합니다.await binding.traceAction(() async {// 성능을 측정할 작업await tester.scrollUntilVisible(itemFinder,500.0,scrollable: listFinder,);}, reportKey: 'scrolling_timeline');
reportKey로 여러 측정을 구분할 수 있습니다.
How
TIP전체 프로파일링 예제
integration_test/scrolling_test.dart import 'package:flutter/material.dart';import 'package:flutter_test/flutter_test.dart';import 'package:integration_test/integration_test.dart';import 'package:your_package/main.dart';void main() {final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();testWidgets('scrolling performance test', (tester) async {await tester.pumpWidget(MyApp(items: List<String>.generate(10000, (i) => 'Item $i')),);final listFinder = find.byType(Scrollable);final itemFinder = find.byKey(const ValueKey('item_50_text'));// 스크롤 성능 측정await binding.traceAction(() async {await tester.scrollUntilVisible(itemFinder,500.0,scrollable: listFinder,);}, reportKey: 'scrolling_timeline');});}결과 저장을 위한 드라이버 파일:
test_driver/perf_driver.dart import 'package:flutter_driver/flutter_driver.dart' as driver;import 'package:integration_test/integration_test_driver.dart';Future<void> main() {return integrationDriver(responseDataCallback: (data) async {if (data != null) {final timeline = driver.Timeline.fromJson(data['scrolling_timeline'] as Map<String, dynamic>,);final summary = driver.TimelineSummary.summarize(timeline);// 타임라인과 요약을 파일로 저장await summary.writeTimelineToFile('scrolling_timeline',pretty: true,includeSummary: true,);}},);}실행:
Terminal window flutter drive \--driver=test_driver/perf_driver.dart \--target=integration_test/scrolling_test.dart \--profile # 프로필 모드로 빌드
Watch out
WARNING—profile 옵션 필수: 디버그 모드는 성능이 낮으므로 정확한 측정이 안 됩니다. 항상
--profile옵션을 사용하세요.모바일에서 —no-dds 옵션: 모바일 기기에서 실행할 때 DDS 비활성화가 필요할 수 있습니다.
Terminal window flutter drive --no-dds --profile ...
챕터 5: 결과 분석
Why
NOTE프로파일링 결과는 두 가지 파일로 저장됩니다.
*_summary.timeline_summary.json: 성능 요약*_timeline.timeline.json: 상세 타임라인 데이터
What
NOTE성능 요약 예시:
{"average_frame_build_time_millis": 4.26,"worst_frame_build_time_millis": 21.0,"missed_frame_build_budget_count": 2,"average_frame_rasterizer_time_millis": 5.52,"worst_frame_rasterizer_time_millis": 51.0,"missed_frame_rasterizer_budget_count": 10,"frame_count": 54}주요 지표:
지표 의미 목표 average_frame_build_time평균 빌드 시간 <8ms worst_frame_build_time최악의 빌드 시간 <16ms missed_frame_build_budget_count예산 초과 프레임 수 0에 가깝게 average_frame_rasterizer_time평균 래스터라이저 시간 <8ms
How
TIP상세 타임라인 분석
chrome://tracing에서 타임라인 JSON 파일을 열 수 있습니다.
- Chrome 브라우저에서
chrome://tracing열기- “Load” 버튼 클릭
*_timeline.timeline.json파일 선택- 타임라인에서 긴 프레임 찾기
CI에서 성능 회귀 감지:
// 성능 기준값 검사final summary = driver.TimelineSummary.summarize(timeline);final avgBuildTime = summary.computeAverageFrameBuildTimeMillis();if (avgBuildTime > 8.0) {throw Exception('Performance regression: avg build time is $avgBuildTime ms');}
챕터 6: Firebase Test Lab 연동
Why
NOTE실제 기기가 없어도 다양한 디바이스에서 테스트할 수 있습니다. Firebase Test Lab은 수백 종류의 실제 Android/iOS 기기를 제공합니다.
What
NOTEAndroid APK 빌드:
Terminal window cd android# 디버그 APK 빌드flutter build apk --debug# 테스트 APK 빌드./gradlew app:assembleAndroidTest# 통합 테스트용 APK 빌드./gradlew app:assembleDebug -Ptarget=integration_test/app_test.dartFirebase Console 업로드:
- Firebase Console → Quality → Test Lab
- “Run a test” 클릭
- Instrumentation 선택
- App APK와 Test APK 업로드
How
TIPFirebase CLI로 자동화:
Terminal window # Firebase CLI 설치npm install -g firebase-tools# 로그인firebase login# 테스트 실행gcloud firebase test android run \--type instrumentation \--app build/app/outputs/apk/debug/app-debug.apk \--test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk \--timeout 5mCI/CD 통합 (GitHub Actions 예시):
- name: Run integration tests on Firebase Test Labrun: |gcloud firebase test android run \--type instrumentation \--app app-debug.apk \--test app-debug-androidTest.apk \--device model=Pixel6,version=33
한계
통합 테스트는 강력하지만 몇 가지 제한이 있습니다.
실행 시간: 단위/위젯 테스트보다 훨씬 느립니다. 모든 테스트를 통합 테스트로 작성하면 안 됩니다.
웹 성능 프로파일링: 웹에서는 traceAction() 성능 측정이 지원되지 않습니다. 웹 성능은 Chrome DevTools를 사용하세요.
테스트 피라미드 권장 비율:
/\ / \ 통합 테스트 (10%) /----\ / \ 위젯 테스트 (30%)/--------\ 단위 테스트 (60%)다음 튜토리얼에서는 플러그인 코드를 테스트하는 방법을 알아봅니다.
Footnotes
공유
이 글이 도움이 되었다면 다른 사람과 공유해주세요!