Flutter E2E 테스트 자동화 및 성능 프로파일링

대 애플리케이션 개발에서 크로스 플랫폼 프레임워크의 도입은 생산성을 극대화하지만, 동시에 QA(Quality Assurance) 복잡도를 증가시킵니다. Flutter는 단일 코드베이스로 다중 플랫폼을 지원하므로, 비즈니스 로직과 UI 렌더링이 다양한 디바이스 환경에서 의도한 대로 동작하는지 검증하는 절차가 필수적입니다. 유닛 테스트(Unit Test)나 위젯 테스트(Widget Test)만으로는 실제 사용자 시나리오에서 발생하는 데이터 흐름이나 네이티브 통합 문제를 완벽히 잡아내기 어렵습니다. 본 글에서는 Flutter의 통합 테스트 도구인 flutter_driver를 사용하여 견고한 End-to-End(E2E) 테스트 환경을 구축하고, 유지보수 가능한 테스트 아키텍처를 설계하는 방법을 기술적 관점에서 분석합니다.

1. Flutter Driver 아키텍처 및 동작 원리

flutter_driver는 단순한 UI 매크로 도구가 아닙니다. 이는 애플리케이션과 테스트 코드가 서로 다른 프로세스에서 실행되는 Out-of-Process 모델을 채택하고 있습니다. 이 구조는 테스트 스크립트가 앱의 내부 메모리에 직접 접근하지 못하게 격리함으로써, 실제 사용자와 가장 유사한 환경에서 블랙박스 테스트(Black-box Testing)를 수행할 수 있게 합니다.

시스템은 크게 두 가지 컴포넌트로 구성됩니다.

  • Instrumented App: FlutterDriverExtension이 활성화된 상태로 빌드된 실제 애플리케이션입니다. VM Service를 통해 외부 명령을 수신할 대기 상태를 유지합니다.
  • Test Runner: Dart로 작성된 테스트 스크립트로, 호스트 머신에서 실행됩니다.
Architecture Note: 두 프로세스 간의 통신은 JSON-RPC 프로토콜을 기반으로 WebSocket을 통해 이루어집니다. 테스트 러너가 명령(예: tap)을 전송하면, 앱 내부의 확장이 이를 해석하여 실제 UI 이벤트를 발생시키고 결과를 반환합니다.

2. 테스트 환경 구축 및 설정

E2E 테스트를 시작하기 위해 의존성 설정과 파일 구조를 정의해야 합니다. Flutter 프로젝트는 관례적으로 test_driver 디렉토리를 사용하여 통합 테스트 파일을 관리합니다.

2.1 의존성 및 진입점 구성

pubspec.yamldev_dependenciesflutter_driver 패키지를 추가한 후, 계측된 앱을 실행할 진입점 파일(app.dart)과 테스트 로직 파일(app_test.dart)을 생성합니다.


// test_driver/app.dart
import 'package:flutter_driver/driver_extension.dart';
import 'package:my_app/main.dart' as app;

void main() {
  // 확장을 활성화하여 테스트 드라이버가 앱을 제어할 수 있도록 함
  enableFlutterDriverExtension();
  app.main();
}

위 코드는 테스트 실행 시 앱이 드라이버의 명령을 수신할 수 있도록 브리지 역할을 합니다. 실제 main() 함수 호출 전에 확장을 활성화하는 것이 핵심입니다.

3. 견고한 테스트 스크립트 작성 전략

E2E 테스트의 가장 큰 적은 '불안정성(Flakiness)'입니다. 네트워크 지연이나 렌더링 속도 차이로 인해 로컬에서는 성공하던 테스트가 CI 환경에서는 실패하는 경우가 빈번합니다. 이를 방지하기 위한 핵심 전략을 소개합니다.

3.1 명시적 식별자 사용 (ValueKey)

텍스트나 아이콘으로 위젯을 찾는 방식은 다국어 지원이나 디자인 변경 시 테스트를 깨뜨릴 수 있습니다. Key, 특히 ValueKey를 사용하여 테스트 대상 위젯에 고유 식별자를 부여해야 합니다.


// 앱 코드 (Widget)
FloatingActionButton(
  key: const ValueKey('add_item_btn'), // 고유 키 할당
  onPressed: () {},
  child: const Icon(Icons.add),
);

// 테스트 코드 (Driver)
final addButtonFinder = find.byValueKey('add_item_btn');
await driver.tap(addButtonFinder);

3.2 비동기 UI 동기화 (Waiting Strategy)

sleep() 함수를 사용하여 하드코딩된 시간을 대기하는 것은 안티 패턴입니다. 디바이스 성능에 따라 테스트 결과가 달라지기 때문입니다. 대신 waitFor API를 사용하여 특정 조건이 충족될 때까지 대기해야 합니다.

API 설명 사용 사례
waitFor 위젯이 위젯 트리에 나타날 때까지 대기 페이지 이동, 비동기 데이터 로딩 후
waitForAbsent 위젯이 사라질 때까지 대기 로딩 인디케이터, 토스트 메시지 소멸 확인
waitUntilNoTransientCallbacks 애니메이션 등의 작업이 끝날 때까지 대기 복잡한 화면 전환이나 스크롤 직후

4. Page Object Model (POM) 패턴 적용

테스트 케이스가 늘어날수록 중복 코드가 발생하고 유지보수가 어려워집니다. 웹 테스트 자동화에서 널리 쓰이는 Page Object Model(POM) 패턴을 적용하여, 페이지의 UI 요소와 동작을 별도의 클래스로 추상화하는 것이 좋습니다.


// test_driver/pages/login_page.dart
class LoginPage {
  final FlutterDriver _driver;

  LoginPage(this._driver);

  final _emailField = find.byValueKey('email_input');
  final _loginBtn = find.byValueKey('login_btn');

  Future<void> login(String email, String password) async {
    await _driver.tap(_emailField);
    await _driver.enterText(email);
    // ... 비밀번호 입력 로직
    await _driver.tap(_loginBtn);
  }
}

// test_driver/app_test.dart
test('Login Scenario', () async {
  final loginPage = LoginPage(driver);
  await loginPage.login('user@test.com', 'password123');
  // 검증 로직...
});
Best Practice: POM을 적용하면 UI 구조가 변경되었을 때 Page 클래스만 수정하면 되므로, 테스트 코드의 유지보수 비용이 획기적으로 감소합니다.

5. 성능 프로파일링 및 CI/CD 통합

flutter_driver의 강력한 기능 중 하나는 앱의 런타임 성능 데이터를 수집할 수 있다는 점입니다. traceAction 메소드를 사용하여 특정 시나리오 실행 시의 타임라인 데이터를 기록할 수 있습니다.


final timeline = await driver.traceAction(() async {
  await driver.scroll(listFinder, 0, -500, const Duration(milliseconds: 500));
});

// 타임라인 데이터를 JSON 파일로 저장하여 Chrome Tracing 등에서 분석 가능
final summary = new TimelineSummary.summarize(timeline);
await summary.writeSummaryToFile('scrolling_performance', pretty: true);

이러한 테스트는 GitHub Actions나 Jenkins와 같은 CI/CD 파이프라인에 통합되어야 합니다. 헤드리스(Headless) 모드로 에뮬레이터를 구동하고, 커밋이나 PR 발생 시 자동으로 테스트를 수행하여 회귀(Regression)를 방지합니다.

Deprecation Warning: Flutter 팀은 장기적으로 flutter_driver 대신 integration_test 패키지 사용을 권장하고 있습니다. integration_testflutter_test API와 호환되며 더 빠른 실행 속도를 제공합니다. 하지만 레거시 프로젝트나 특정 성능 프로파일링 니즈가 있는 경우 flutter_driver는 여전히 유효한 선택지입니다.

결론

E2E 테스트는 초기 구축 비용이 발생하지만, 장기적으로 소프트웨어의 안정성을 보장하는 가장 확실한 투자입니다. flutter_driver를 통해 구축한 자동화 파이프라인은 개발자가 기능 구현에만 집중할 수 있는 환경을 제공합니다. 다만, 테스트 코드 역시 프로덕션 코드와 동일한 수준의 아키텍처 설계(POM 패턴 등)와 관리가 필요함을 잊지 말아야 합니다. 팀의 현재 기술 스택과 요구사항을 분석하여 flutter_driver 혹은 최신 integration_test 패키지를 적절히 선택하여 도입하시기 바랍니다.

Post a Comment