Flutter Integration Test 가이드

범용 개념 + MotionStitch Lite 실습 예시
Flutter Testing E2E Automation

1. Integration Test란?

Flutter 테스트의 3단계

Flutter는 테스트를 세 가지 수준으로 나눈다. 아래로 갈수록 범위가 넓고, 실행 비용이 높다.

종류 대상 실행 환경 속도
Unit Test 함수, 클래스, 메서드 하나 Dart VM (기기 불필요) 매우 빠름
Widget Test 단일 위젯 렌더링 + 상호작용 Dart VM (기기 불필요) 빠름
Integration Test 앱 전체 또는 큰 기능 흐름 실기기 또는 에뮬레이터 필수 느림 (수십 초~수 분)

Integration Test의 특징

2. 왜 필요한가?

수동 테스트의 문제점

Integration Test가 해결하는 것

3. 세팅 방법

Step 1: pubspec.yaml에 의존성 추가

integration_test는 Flutter SDK에 내장되어 있다. 별도 버전 지정 없이 sdk: flutter로 추가한다.

# pubspec.yaml
dev_dependencies:
  flutter_test:
    sdk: flutter
  integration_test:
    sdk: flutter

추가 후 flutter pub get을 실행한다.

Step 2: 폴더 구조 생성

프로젝트 루트에 다음 두 폴더를 만든다.

my_app/
  integration_test/       # 통합 테스트 파일
    app_test.dart         # 메인 테스트
  test_driver/            # (선택) 드라이버 설정
    integration_test.dart # 드라이버 엔트리포인트
참고: Flutter 2.5 이후로는 test_driver/ 없이도 flutter test integration_test/ 명령으로 직접 실행할 수 있다. test_driver/는 Chrome 등 웹 드라이버가 필요한 경우에만 사용한다.

Step 3: test_driver/integration_test.dart (선택)

웹이 아닌 모바일만 대상이라면 이 파일은 생략 가능하다. 필요 시 다음과 같이 작성한다.

import 'package:integration_test/integration_test_driver.dart';

Future<void> main() => integrationDriver();

4. 테스트 작성법

기본 구조

통합 테스트 파일은 다음 패턴을 따른다.

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();

  testWidgets('홈 화면이 정상 표시된다', (tester) async {
    app.main();
    await tester.pumpAndSettle();

    expect(find.text('환영합니다'), findsOneWidget);
  });
}

위젯 찾기: find

메서드설명예시
find.text('...')정확히 일치하는 텍스트find.text('새로 만들기')
find.textContaining('...')포함하는 텍스트find.textContaining('하이라이트')
find.byType(Widget)위젯 타입으로 찾기find.byType(ElevatedButton)
find.byKey(Key)Key로 찾기find.byKey(Key('submit_btn'))
find.byIcon(icon)아이콘으로 찾기find.byIcon(Icons.add)

검증: expect

매처의미
findsOneWidget정확히 1개 발견
findsWidgets1개 이상 발견
findsNothing0개 (화면에 없음)
findsNWidgets(n)정확히 n개 발견
expect(find.text('출력 설정'), findsOneWidget);
expect(find.text('이전 화면'), findsNothing);

사용자 상호작용: tester

메서드동작
tester.tap(finder)위젯 탭 (클릭)
tester.enterText(finder, text)텍스트 필드에 입력
tester.drag(finder, offset)드래그 (스크롤)
tester.longPress(finder)길게 누르기
tester.pump()한 프레임 렌더
tester.pumpAndSettle()애니메이션 완료까지 대기
// 버튼 탭 후 화면 전환 대기
await tester.tap(find.text('새로 만들기'));
await tester.pumpAndSettle(const Duration(seconds: 2));

// 텍스트 입력
await tester.enterText(find.byType(TextField), '검색어');
await tester.pumpAndSettle();

비동기 대기 패턴

네트워크 요청, 파일 I/O 등 비동기 작업 후에는 pumpAndSettle()만으로 부족할 수 있다. 이 경우 타임아웃을 주거나, 특정 위젯이 나타날 때까지 폴링한다.

// 방법 1: 타임아웃 지정
await tester.pumpAndSettle(const Duration(seconds: 10));

// 방법 2: 특정 위젯이 나타날 때까지 폴링
bool found = false;
for (int i = 0; i < 30; i++) {
  await tester.pump(const Duration(seconds: 1));
  if (find.text('완료').evaluate().isNotEmpty) {
    found = true;
    break;
  }
}
expect(found, isTrue);

group()으로 테스트 묶기

관련된 테스트를 group()으로 묶어서 구조화할 수 있다.

group('앱 시작', () {
  testWidgets('홈 화면이 정상 표시된다', (tester) async {
    // ...
  });

  testWidgets('출력 설정 카드가 표시된다', (tester) async {
    // ...
  });
});

group('화면 전환', () {
  testWidgets('새로 만들기 탭 시 다음 화면으로 이동', (tester) async {
    // ...
  });
});

5. 실행 방법

기본 실행 명령

# 연결된 기기/에뮬레이터에서 실행
flutter test integration_test/app_test.dart

# 특정 디바이스 지정
flutter test integration_test/app_test.dart -d <device-id>

# 에뮬레이터 예시
flutter test integration_test/app_test.dart -d emulator-5554

# 실기기 예시
flutter test integration_test/app_test.dart -d RFKYC0DKY7J

디바이스 목록 확인

# 연결된 디바이스 목록
flutter devices

# 출력 예시:
# Samsung Galaxy S24 (mobile) - RFKYC0DKY7J - android-arm64
# sdk gphone64 arm64 (mobile) - emulator-5554 - android-arm64

특정 테스트만 실행

현재 Flutter integration test에서는 파일 단위로 실행한다. 특정 testWidgets만 골라서 실행하는 기본 옵션은 없으므로, 필요하면 테스트 파일을 분리하거나 skip: true를 활용한다.

testWidgets('건너뛸 테스트', (tester) async {
  // ...
}, skip: true);  // 이 테스트는 건너뜀

6. 실기기 vs 에뮬레이터 차이점

항목에뮬레이터실기기
권한 다이얼로그 자동 승인되는 경우가 많음 시스템 다이얼로그가 실제로 뜸 (자동 제어 불가)
사진/파일 접근 실제 데이터가 거의 없음 사용자의 실제 사진/동영상에 접근 가능
모션포토 모션포토 파일이 없어 스캔 결과 0건 실제 모션포토로 전체 흐름 테스트 가능
카메라 가상 카메라 (시뮬레이션) 실제 카메라 동작
성능 호스트 PC 성능에 의존, 일반적으로 느림 기기 고유 성능, 대체로 빠름
CI/CD 적합도 헤드리스 에뮬레이터로 CI에서 실행 가능 물리적 기기 연결 필요 (Firebase Test Lab 등)
권한 다이얼로그 주의: Android/iOS의 시스템 권한 다이얼로그(사진 접근, 위치 등)는 Flutter의 위젯 트리 밖에 있기 때문에 tester.tap()으로 제어할 수 없다. 실기기에서는 테스트 전에 미리 권한을 허용해 두거나, adb shell pm grant 명령으로 사전 부여하는 방법을 사용한다.

7. MotionStitch Lite 테스트 예시

현재 테스트 파일: integration_test/app_test.dart

MotionStitch Lite에는 3개 그룹, 총 6개의 통합 테스트가 작성되어 있다.

그룹 1: 앱 시작

테스트검증 내용
홈 화면이 정상 표시된다 앱 타이틀(Motion / Stitch), 부제("하이라이트 요약 영상"), 핵심 버튼("새로 만들기", "내 무비 갤러리"), 버전 표시가 모두 존재하는지 확인
출력 설정 카드가 표시된다 "출력 설정", "화면 비율", "해상도" 라벨과 선택지(4:3 가로, 4:3 세로, 원본, FHD, HD)가 표시되는지 확인

그룹 2: 화면 전환

테스트검증 내용
새로 만들기 -> 모션포토 선택 화면 "새로 만들기" 탭 후 홈 화면의 "출력 설정"이 사라지는지 확인. 권한 다이얼로그가 뜨면 자동 제어 불가이므로, 화면이 바뀐 것만 최소 검증
내 무비 갤러리 -> 갤러리 화면 "내 무비 갤러리" 탭 후 갤러리 화면으로 이동하는지 확인

그룹 3: 출력 설정 인터랙션

테스트검증 내용
화면 비율을 4:3 세로로 변경 "4:3 세로" 탭 후 해상도 표시(NxM 형태)가 변경되는지 확인
해상도를 FHD로 변경 "FHD" 탭 후 "1440x1080" 텍스트가 표시되는지 확인
해상도를 HD로 변경 "HD" 탭 후 "960x720" 텍스트가 표시되는지 확인

한계 및 향후 과제

8. 팁

pumpAndSettle 타임아웃

기본 타임아웃은 매우 짧다. 앱 초기화, 네트워크 요청, 무거운 처리가 있으면 충분한 타임아웃을 준다.

await tester.pumpAndSettle(
  const Duration(seconds: 10),
);

Provider 상태 변경 후

Provider로 상태를 변경한 후에는 반드시 pump()를 호출하여 UI가 리빌드되도록 해야 한다.

await tester.tap(find.text('옵션'));
// 상태 변경 후 UI 갱신 대기
await tester.pumpAndSettle();

시스템 다이얼로그 제한

권한 요청, 파일 선택기(SAF), 공유 시트 등은 OS 수준 UI이므로 Flutter Integration Test로 제어할 수 없다.

  • 사전에 adb shell pm grant로 권한 부여
  • Mock 플러그인으로 대체
  • 해당 부분은 수동 테스트로 보완

CI/CD 연동

자동화된 테스트 파이프라인에서 실행할 수 있다.

  • GitHub Actions: 에뮬레이터를 띄우고 테스트 실행
  • Firebase Test Lab: 실기기 팜에서 테스트
  • Codemagic: Flutter 특화 CI 서비스

기타 유용한 팁