Flutter 튜토리얼 47편: 플러그인 테스팅

요약#

핵심 요지#

  • 문제 정의: 플러그인은 네이티브 코드(Kotlin, Swift)를 포함하므로 단위/위젯 테스트에서 MissingPluginException1이 발생한다.
  • 핵심 주장: 플러그인 테스트는 Dart 테스트, 통합 테스트, 네이티브 단위 테스트 세 가지를 조합해서 수행한다.
  • 주요 근거: 각 테스트 유형이 다른 범위를 커버하므로 조합해야 완전한 커버리지를 얻을 수 있다.
  • 실무 기준: 플러그인을 사용하는 앱 테스트에서는 플러그인 API를 래핑하거나 목(mock)으로 대체한다.
  • 한계: 네이티브 UI를 테스트하려면 Espresso나 XCUITest 같은 별도 프레임워크가 필요하다.

문서가 설명하는 범위#

  • 플러그인 테스트의 세 가지 유형
  • Dart 단위 테스트에서 플러그인 목 처리
  • 네이티브 단위 테스트 (JUnit, XCTest, GoogleTest)
  • 통합 테스트로 전체 플러그인 검증
  • 플러그인을 사용하는 앱 테스트 전략

읽는 시간: 18분 | 난이도: 고급


참고 자료#


문제 상황#

플러그인은 네이티브 코드를 포함합니다. 단위 테스트나 위젯 테스트에서 플러그인을 호출하면 에러가 발생합니다.

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에서 실행됩니다. 네이티브 코드가 로드되지 않아서 플러그인 호출이 실패합니다.


해결 방법#

플러그인 테스트에는 세 가지 전략이 있습니다.

graph TD A[플러그인 테스트 전략] --> B[Dart 테스트] A --> C[통합 테스트] A --> D[네이티브 단위 테스트] B --> E[Dart 로직만 테스트] B --> F[플랫폼 채널 목 필요] C --> G[전체 플러그인 테스트] C --> H[실제 기기에서 실행] D --> I[네이티브 로직 테스트] D --> J[각 플랫폼별 프레임워크]

챕터 1: 플러그인을 사용하는 앱 테스트#

Why#

NOTE

플러그인 개발자가 아니어도 플러그인을 사용하는 코드를 테스트해야 합니다. MissingPluginException을 피하는 방법이 필요합니다.

What#

NOTE

우선순위별 해결 방법:

  1. 플러그인 래핑 (권장): 플러그인 호출을 자체 API로 감싸고 목으로 대체
  2. 플러그인 공개 API 목: 플러그인이 클래스 기반이면 직접 목
  3. 플랫폼 인터페이스 목: 연합 플러그인의 내부 인터페이스 목
  4. 플랫폼 채널 목: 최후의 수단, 내부 구현에 의존

How#

TIP

방법 1: 플러그인 래핑 (권장)

플러그인 호출을 자체 클래스로 감쌉니다.

// 플러그인을 래핑한 서비스 클래스
abstract class LocationService {
Future<Position> getCurrentLocation();
}
// 실제 구현 (프로덕션용)
class RealLocationService implements LocationService {
@override
Future<Position> getCurrentLocation() {
return Geolocator.getCurrentPosition();
}
}
// 테스트용 목
class MockLocationService implements LocationService {
@override
Future<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#

WARNING

Mocktail/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_plugin
cd my_plugin

템플릿이 각 테스트 디렉토리를 자동 생성합니다.


챕터 3: Dart 단위 테스트 작성#

Why#

NOTE

Dart 테스트는 플러그인의 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 test

IDE(VS Code, Android Studio)에서도 직접 실행할 수 있습니다.

Watch out#

WARNING

플랫폼 채널 목의 단점:

  • 채널 이름과 메서드 이름이 내부 구현 세부사항
  • 플러그인 업데이트 시 테스트가 깨질 수 있음
  • 플랫폼별로 구현이 다를 수 있음

가능하면 플랫폼 인터페이스 목을 사용하세요.

// 연합 플러그인의 경우 플랫폼 인터페이스 목 사용
class MockBatteryPlatform extends Mock
with MockPlatformInterfaceMixin
implements 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 example
flutter test integration_test

특정 플랫폼에서 실행:

Terminal window
# Android
flutter test integration_test -d android
# iOS
flutter 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

플랫폼별 테스트 프레임워크:

플랫폼프레임워크테스트 위치
AndroidJUnitandroid/src/test/
iOSXCTestexample/ios/RunnerTests/
macOSXCTestexample/macos/RunnerTests/
LinuxGoogleTestlinux/test/
WindowsGoogleTestwindows/test/

How#

TIP

Android JUnit 테스트 예시:

android/src/test/kotlin/com/example/my_plugin/MyPluginTest.kt
package com.example.my_plugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import org.junit.Test
import org.mockito.Mockito.*
class MyPluginTest {
@Test
fun 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 testDebugUnitTest

iOS XCTest 테스트 예시:

example/ios/RunnerTests/MyPluginTests.swift
import XCTest
@testable import my_plugin
class MyPluginTests: XCTestCase {
func testGetBatteryLevel() {
let plugin = MyPlugin()
let expectation = expectation(description: "Battery level returned")
plugin.handle(
FlutterMethodCall(methodName: "getBatteryLevel", arguments: nil)
) { result in
XCTAssertNotNil(result)
if let level = result as? Int {
XCTAssert(level >= 0 && level <= 100)
}
expectation.fulfill()
}
waitForExpectations(timeout: 1.0)
}
}

실행:

Terminal window
cd example/ios
xcodebuild test -workspace Runner.xcworkspace -scheme Runner -configuration Debug

Watch out#

WARNING

빌드 순서 주의: 네이티브 테스트 실행 전에 예제 앱을 한 번 빌드해야 합니다.

Terminal window
cd example
flutter build ios # 또는 android, linux, windows

이렇게 해야 플랫폼별 빌드 파일이 생성됩니다.

코드 서명 (iOS): Xcode에서 Runner.xcworkspace를 열어 코드 서명을 설정해야 할 수 있습니다.


챕터 6: 테스트 전략 권장사항#

Why#

NOTE

어떤 테스트를 얼마나 작성해야 할까요? 플러그인 테스트는 일반 Flutter 앱과 다른 전략이 필요합니다.

What#

NOTE

권장 테스트 전략:

  1. 모든 플랫폼 채널 호출에 통합 테스트 작성

    • Dart와 네이티브 간의 통신 검증
    • 가장 중요한 테스트
  2. 네이티브 UI나 기기 상태가 필요하면 네이티브 테스트 추가

    • 통합 테스트로 불가능한 시나리오
    • 네이티브 API 목 필요
  3. 복잡한 Dart 로직은 Dart 테스트로 분리

    • 플랫폼 채널과 무관한 순수 Dart 로직

How#

TIP

테스트 커버리지 예시:

my_camera_plugin/
├── Dart 테스트
│ ├── 이미지 처리 로직
│ └── 설정 파싱 로직
├── 통합 테스트
│ ├── 카메라 초기화
│ ├── 사진 촬영
│ ├── 비디오 녹화
│ └── 권한 요청 흐름
└── 네이티브 테스트
├── Android: 카메라 API 목으로 에러 처리 테스트
└── iOS: AVFoundation 목으로 권한 거부 시나리오

한계#

플러그인 테스트에는 몇 가지 한계가 있습니다.

네이티브 UI 테스트: 네이티브 다이얼로그나 플랫폼 뷰 내용은 통합 테스트로 검증할 수 없습니다. Espresso(Android)나 XCUITest(iOS) 같은 별도 프레임워크가 필요합니다.

Dart-only 구현 주의: 일부 플러그인은 특정 플랫폼에서 Dart-only 구현을 사용합니다. 이 경우 단위 테스트에서도 실행되지만, 구현 세부사항이므로 의존하면 안 됩니다.

플랫폼별 동작 차이: 같은 플러그인이라도 플랫폼마다 동작이 다를 수 있습니다. 각 플랫폼에서 테스트를 실행하세요.

다음 튜토리얼에서는 디버깅 도구 활용 방법을 알아봅니다.

Footnotes#

  1. MissingPluginException(미싱플러그인익셉션): 플러그인의 네이티브 구현이 로드되지 않아 플랫폼 채널 호출이 실패할 때 발생하는 예외다.

공유

이 글이 도움이 되었다면 다른 사람과 공유해주세요!

Flutter 튜토리얼 47편: 플러그인 테스팅
https://moodturnpost.net/posts/flutter/flutter-testing-plugins/
작성자
Moodturn
게시일
2026-01-08
Moodturn

목차