Flutter 튜토리얼 47편: 플러그인 테스팅
요약
핵심 요지
- 문제 정의: 플러그인은 네이티브 코드(Kotlin, Swift)를 포함하므로 단위/위젯 테스트에서
MissingPluginException1이 발생한다. - 핵심 주장: 플러그인 테스트는 Dart 테스트, 통합 테스트, 네이티브 단위 테스트 세 가지를 조합해서 수행한다.
- 주요 근거: 각 테스트 유형이 다른 범위를 커버하므로 조합해야 완전한 커버리지를 얻을 수 있다.
- 실무 기준: 플러그인을 사용하는 앱 테스트에서는 플러그인 API를 래핑하거나 목(mock)으로 대체한다.
- 한계: 네이티브 UI를 테스트하려면 Espresso나 XCUITest 같은 별도 프레임워크가 필요하다.
문서가 설명하는 범위
- 플러그인 테스트의 세 가지 유형
- Dart 단위 테스트에서 플러그인 목 처리
- 네이티브 단위 테스트 (JUnit, XCTest, GoogleTest)
- 통합 테스트로 전체 플러그인 검증
- 플러그인을 사용하는 앱 테스트 전략
읽는 시간: 18분 | 난이도: 고급
참고 자료
- Testing plugins - 플러그인 테스트 가이드
- Plugins in Flutter tests - 테스트에서 플러그인 처리 방법
문제 상황
플러그인은 네이티브 코드를 포함합니다. 단위 테스트나 위젯 테스트에서 플러그인을 호출하면 에러가 발생합니다.
MissingPluginException 발생
// 플러그인을 사용하는 코드class LocationService { Future<Position> getCurrentLocation() async { // geolocator 플러그인 호출 return await Geolocator.getCurrentPosition(); }}
// 단위 테스트test('get current location', () async { final service = LocationService();
// 에러 발생! // MissingPluginException(No implementation found for method // getCurrentPosition on channel flutter.baseflow.com/geolocator) await service.getCurrentLocation();});단위/위젯 테스트는 Flutter 엔진 없이 Dart VM에서 실행됩니다. 네이티브 코드가 로드되지 않아서 플러그인 호출이 실패합니다.
해결 방법
플러그인 테스트에는 세 가지 전략이 있습니다.
챕터 1: 플러그인을 사용하는 앱 테스트
Why
NOTE플러그인 개발자가 아니어도 플러그인을 사용하는 코드를 테스트해야 합니다.
MissingPluginException을 피하는 방법이 필요합니다.
What
NOTE우선순위별 해결 방법:
- 플러그인 래핑 (권장): 플러그인 호출을 자체 API로 감싸고 목으로 대체
- 플러그인 공개 API 목: 플러그인이 클래스 기반이면 직접 목
- 플랫폼 인터페이스 목: 연합 플러그인의 내부 인터페이스 목
- 플랫폼 채널 목: 최후의 수단, 내부 구현에 의존
How
TIP방법 1: 플러그인 래핑 (권장)
플러그인 호출을 자체 클래스로 감쌉니다.
// 플러그인을 래핑한 서비스 클래스abstract class LocationService {Future<Position> getCurrentLocation();}// 실제 구현 (프로덕션용)class RealLocationService implements LocationService {@overrideFuture<Position> getCurrentLocation() {return Geolocator.getCurrentPosition();}}// 테스트용 목class MockLocationService implements LocationService {@overrideFuture<Position> getCurrentLocation() async {return Position(latitude: 37.5665,longitude: 126.9780,timestamp: DateTime.now(),accuracy: 10.0,altitude: 0.0,heading: 0.0,speed: 0.0,speedAccuracy: 0.0,altitudeAccuracy: 0.0,headingAccuracy: 0.0,);}}테스트에서 목을 주입합니다.
test('show current location', () async {final service = MockLocationService();final viewModel = LocationViewModel(service);await viewModel.loadLocation();expect(viewModel.latitude, 37.5665);expect(viewModel.longitude, 126.9780);});장점:
- 플러그인 API 변경에 영향 받지 않음
- 자체 코드만 테스트하므로 테스트 범위가 명확
- 모든 플러그인에 동일하게 적용 가능
Watch out
WARNINGMocktail/Mockito 사용 시:
// mocktail 사용 예시import 'package:mocktail/mocktail.dart';class MockLocationService extends Mock implements LocationService {}test('handle location error', () async {final service = MockLocationService();// 에러 상황 시뮬레이션when(() => service.getCurrentLocation()).thenThrow(LocationServiceDisabledException());final viewModel = LocationViewModel(service);expect(() => viewModel.loadLocation(),throwsA(isA<LocationServiceDisabledException>()),);});
챕터 2: 플러그인 개발자를 위한 테스트
Why
NOTE플러그인을 개발할 때는 세 가지 테스트를 모두 작성해야 합니다.
- Dart 테스트: Dart 로직 검증
- 통합 테스트: 전체 플러그인 동작 검증
- 네이티브 테스트: 네이티브 코드 검증
What
NOTE플러그인 템플릿의 디렉토리 구조입니다.
my_plugin/├── lib/ # Dart 코드├── test/ # Dart 단위 테스트├── example/│ ├── integration_test/ # 통합 테스트│ ├── ios/RunnerTests/ # iOS XCTest│ └── macos/RunnerTests/ # macOS XCTest├── android/src/test/ # Android JUnit├── linux/test/ # Linux GoogleTest└── windows/test/ # Windows GoogleTest
How
TIP플러그인 프로젝트 생성:
Terminal window flutter create --template=plugin my_plugincd my_plugin템플릿이 각 테스트 디렉토리를 자동 생성합니다.
챕터 3: Dart 단위 테스트 작성
Why
NOTEDart 테스트는 플러그인의 Dart 부분만 테스트합니다. 네이티브 코드는 실행되지 않으므로 플랫폼 채널을 목으로 대체해야 합니다.
What
NOTE플랫폼 채널 목 처리 방법입니다.
import 'package:flutter/services.dart';import 'package:flutter_test/flutter_test.dart';void main() {// 테스트 바인딩 초기화TestWidgetsFlutterBinding.ensureInitialized();test('getBatteryLevel returns value from platform', () async {// 플랫폼 채널 목 설정const channel = MethodChannel('samples.flutter.dev/battery');TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, (MethodCall methodCall) async {if (methodCall.method == 'getBatteryLevel') {return 42; // 목 응답}return null;});// 테스트 실행final battery = BatteryPlugin();final level = await battery.getBatteryLevel();expect(level, 42);});}
How
TIP플러그인 테스트 실행:
Terminal window # 프로젝트 루트에서flutter testIDE(VS Code, Android Studio)에서도 직접 실행할 수 있습니다.
Watch out
WARNING플랫폼 채널 목의 단점:
- 채널 이름과 메서드 이름이 내부 구현 세부사항
- 플러그인 업데이트 시 테스트가 깨질 수 있음
- 플랫폼별로 구현이 다를 수 있음
가능하면 플랫폼 인터페이스 목을 사용하세요.
// 연합 플러그인의 경우 플랫폼 인터페이스 목 사용class MockBatteryPlatform extends Mockwith MockPlatformInterfaceMixinimplements BatteryPlatform {}void main() {test('getBatteryLevel returns platform value', () async {final mock = MockBatteryPlatform();BatteryPlatform.instance = mock;when(() => mock.getBatteryLevel()).thenAnswer((_) async => 42);final level = await Battery().getBatteryLevel();expect(level, 42);});}
챕터 4: 통합 테스트 작성
Why
NOTE통합 테스트는 Dart와 네이티브 코드 간의 상호작용을 테스트합니다. 실제 기기에서 실행되므로 전체 플러그인 동작을 검증할 수 있습니다.
플러그인 테스트에서 가장 중요한 테스트입니다.
What
NOTE통합 테스트는
example디렉토리에서 작성합니다.example/integration_test/plugin_test.dart import 'package:flutter_test/flutter_test.dart';import 'package:integration_test/integration_test.dart';import 'package:my_plugin/my_plugin.dart';void main() {IntegrationTestWidgetsFlutterBinding.ensureInitialized();testWidgets('getBatteryLevel returns valid value', (tester) async {final plugin = MyPlugin();final batteryLevel = await plugin.getBatteryLevel();// 배터리 레벨은 0-100 사이여야 함expect(batteryLevel, greaterThanOrEqualTo(0));expect(batteryLevel, lessThanOrEqualTo(100));});}
How
TIP통합 테스트 실행:
Terminal window cd exampleflutter test integration_test특정 플랫폼에서 실행:
Terminal window # Androidflutter test integration_test -d android# iOSflutter test integration_test -d ios# 웹 (ChromeDriver 필요)flutter drive \--driver=test_driver/integration_test.dart \--target=integration_test/plugin_test.dart \-d chrome
Watch out
WARNING통합 테스트 한계:
- 네이티브 UI와 상호작용 불가 (네이티브 다이얼로그, 플랫폼 뷰 내용)
- 기기 상태 목 불가 (배터리 레벨, 네트워크 상태 등)
이런 경우 네이티브 단위 테스트를 사용하세요.
챕터 5: 네이티브 단위 테스트
Why
NOTE네이티브 단위 테스트는 네이티브 코드를 독립적으로 테스트합니다. 기기 API를 목으로 대체할 수 있어서 다양한 시나리오를 테스트할 수 있습니다.
What
NOTE플랫폼별 테스트 프레임워크:
플랫폼 프레임워크 테스트 위치 Android JUnit android/src/test/iOS XCTest example/ios/RunnerTests/macOS XCTest example/macos/RunnerTests/Linux GoogleTest linux/test/Windows GoogleTest windows/test/
How
TIPAndroid JUnit 테스트 예시:
android/src/test/kotlin/com/example/my_plugin/MyPluginTest.kt package com.example.my_pluginimport io.flutter.plugin.common.MethodCallimport io.flutter.plugin.common.MethodChannelimport org.junit.Testimport org.mockito.Mockito.*class MyPluginTest {@Testfun getBatteryLevel_returnsExpectedValue() {val plugin = MyPlugin()val mockResult = mock(MethodChannel.Result::class.java)plugin.onMethodCall(MethodCall("getBatteryLevel", null),mockResult)verify(mockResult).success(anyInt())}}실행:
Terminal window cd example/android./gradlew testDebugUnitTestiOS XCTest 테스트 예시:
example/ios/RunnerTests/MyPluginTests.swift import XCTest@testable import my_pluginclass MyPluginTests: XCTestCase {func testGetBatteryLevel() {let plugin = MyPlugin()let expectation = expectation(description: "Battery level returned")plugin.handle(FlutterMethodCall(methodName: "getBatteryLevel", arguments: nil)) { result inXCTAssertNotNil(result)if let level = result as? Int {XCTAssert(level >= 0 && level <= 100)}expectation.fulfill()}waitForExpectations(timeout: 1.0)}}실행:
Terminal window cd example/iosxcodebuild test -workspace Runner.xcworkspace -scheme Runner -configuration Debug
Watch out
WARNING빌드 순서 주의: 네이티브 테스트 실행 전에 예제 앱을 한 번 빌드해야 합니다.
Terminal window cd exampleflutter build ios # 또는 android, linux, windows이렇게 해야 플랫폼별 빌드 파일이 생성됩니다.
코드 서명 (iOS): Xcode에서
Runner.xcworkspace를 열어 코드 서명을 설정해야 할 수 있습니다.
챕터 6: 테스트 전략 권장사항
Why
NOTE어떤 테스트를 얼마나 작성해야 할까요? 플러그인 테스트는 일반 Flutter 앱과 다른 전략이 필요합니다.
What
NOTE권장 테스트 전략:
모든 플랫폼 채널 호출에 통합 테스트 작성
- Dart와 네이티브 간의 통신 검증
- 가장 중요한 테스트
네이티브 UI나 기기 상태가 필요하면 네이티브 테스트 추가
- 통합 테스트로 불가능한 시나리오
- 네이티브 API 목 필요
복잡한 Dart 로직은 Dart 테스트로 분리
- 플랫폼 채널과 무관한 순수 Dart 로직
How
TIP테스트 커버리지 예시:
my_camera_plugin/├── Dart 테스트│ ├── 이미지 처리 로직│ └── 설정 파싱 로직│├── 통합 테스트│ ├── 카메라 초기화│ ├── 사진 촬영│ ├── 비디오 녹화│ └── 권한 요청 흐름│└── 네이티브 테스트├── Android: 카메라 API 목으로 에러 처리 테스트└── iOS: AVFoundation 목으로 권한 거부 시나리오
한계
플러그인 테스트에는 몇 가지 한계가 있습니다.
네이티브 UI 테스트: 네이티브 다이얼로그나 플랫폼 뷰 내용은 통합 테스트로 검증할 수 없습니다. Espresso(Android)나 XCUITest(iOS) 같은 별도 프레임워크가 필요합니다.
Dart-only 구현 주의: 일부 플러그인은 특정 플랫폼에서 Dart-only 구현을 사용합니다. 이 경우 단위 테스트에서도 실행되지만, 구현 세부사항이므로 의존하면 안 됩니다.
플랫폼별 동작 차이: 같은 플러그인이라도 플랫폼마다 동작이 다를 수 있습니다. 각 플랫폼에서 테스트를 실행하세요.
다음 튜토리얼에서는 디버깅 도구 활용 방법을 알아봅니다.
Footnotes
-
MissingPluginException(미싱플러그인익셉션): 플러그인의 네이티브 구현이 로드되지 않아 플랫폼 채널 호출이 실패할 때 발생하는 예외다. ↩
공유
이 글이 도움이 되었다면 다른 사람과 공유해주세요!