Files
Musify/musify_flutter/.agents/skills/flutter-testing/references/plugin-testing.md
T

11 KiB

Plugin Testing Guide

Overview

Flutter plugins require special testing strategies because they include native code. This guide covers testing plugin packages with Dart, native, and integration tests.

Test Types for Plugins

Type Tests Runs On Purpose
Dart Unit Single classes/functions Dart VM Test Dart code in isolation
Dart Widget UI components Test environment Test widget behavior
Dart Integration Dart + Native bridge Device/Emulator Test full plugin functionality
Native Unit Native code Native test env Test native code in isolation
Native UI Native UI + Flutter UI Device/Emulator Test native UI interactions

Project Structure

my_plugin/
├── lib/                    # Dart code
│   └── my_plugin.dart
├── android/                 # Android native code
│   └── src/test/           # Android unit tests
├── ios/                    # iOS native code
├── example/                 # Example app
│   ├── integration_test/      # Integration tests
│   ├── ios/RunnerTests/     # iOS tests
│   └── lib/
├── test/                    # Dart unit tests
│   └── my_plugin_test.dart
└── pubspec.yaml

Dart Unit Tests

Basic Plugin Unit Test

import 'package:flutter_test/flutter_test.dart';
import 'package:my_plugin/my_plugin.dart';

void main() {
  test('plugin initializes correctly', () {
    final plugin = MyPlugin();
    expect(plugin.initialized, false);
    
    plugin.initialize();
    expect(plugin.initialized, true);
  });
  
  test('processes data correctly', () {
    final plugin = MyPlugin();
    final result = plugin.processData([1, 2, 3]);
    
    expect(result, [2, 4, 6]);
  });
}

Mocking Platform Channels in Unit Tests

import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_plugin/my_plugin.dart';

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();
  
  late MyPlugin plugin;
  
  setUp(() {
    // Mock platform channel
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .setMockMethodCallHandler(
      const MethodChannel('com.example.my_plugin'),
      (MethodCall methodCall) async {
        if (methodCall.method == 'getNativeValue') {
          return 42;
        }
        return null;
      },
    );
    
    plugin = MyPlugin();
  });
  
  tearDown(() {
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .setMockMethodCallHandler(
      const MethodChannel('com.example.my_plugin'),
      null,
    );
  });
  
  test('gets value from native side', () async {
    final result = await plugin.getNativeValue();
    expect(result, 42);
  });
}

Dart Integration Tests

Setup

Add to example/pubspec.yaml:

dev_dependencies:
  integration_test:
    sdk: flutter
  flutter_test:
    sdk: flutter
  my_plugin:
    path: ../

Basic Integration Test

// example/integration_test/my_plugin_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_plugin_example/main.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
  
  testWidgets('plugin works in real app', (tester) async {
    await tester.pumpWidget(const MyApp());
    
    final plugin = MyPlugin();
    final result = await plugin.getNativeValue();
    
    expect(result, greaterThan(0));
  });
}

Create Test Driver

// test_driver/integration_test.dart
import 'package:integration_test/integration_test_driver.dart';

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

Run Integration Tests

# From example directory
cd example
flutter test integration_test/

# Or with driver
flutter drive --target=integration_test/my_plugin_test.dart

Android Unit Tests

Setup

Create test in android/src/test/java/com/example/my_plugin/MyPluginTest.java:

package com.example.my_plugin;

import org.junit.Test;
import static org.junit.Assert.*;

public class MyPluginTest {
    
    @Test
    public void testNativeMethod() {
        MyPlugin plugin = new MyPlugin();
        int result = plugin.calculate(10, 20);
        
        assertEquals(30, result);
    }
    
    @Test
    public void testStringProcessing() {
        MyPlugin plugin = new MyPlugin();
        String result = plugin.process("hello");
        
        assertEquals("HELLO", result);
    }
}

Run Android Tests

# From android directory
cd example/android

# Run all tests
./gradlew testDebugUnitTest

# Run specific test
./gradlew test --tests MyPluginTest.testNativeMethod

iOS Unit Tests

Setup

Create test in example/ios/RunnerTests/RunnerTests.m:

#import <XCTest/XCTest.h>
#import "MyPlugin.h"

@interface MyPluginTest : XCTestCase
@end

@implementation MyPluginTest

- (void)setUp {
    [super setUp];
    // Setup code
}

- (void)testNativeMethod {
    MyPlugin *plugin = [[MyPlugin alloc] init];
    NSInteger result = [plugin calculateWithA:10 b:20];
    
    XCTAssertEqual(result, 30);
}

- (void)testStringProcessing {
    MyPlugin *plugin = [[MyPlugin alloc] init];
    NSString *result = [plugin processString:@"hello"];
    
    XCTAssertEqualObjects(result, @"HELLO");
}

@end

Run iOS Tests

# From ios directory
cd example/ios

# Run tests
xcodebuild test -workspace Runner.xcworkspace -scheme Runner -configuration Debug

# Or run from Xcode
# Product > Test

Native UI Tests (Espresso/XCUITest)

Android Espresso Test

// android/src/androidTest/java/com/example/my_plugin/UiTest.java
package com.example.my_plugin;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.rule.ActivityTestRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

@RunWith(AndroidJUnit4.class)
public class UiTest {
    
    @Rule
    public ActivityTestRule<MainActivity> activityRule = 
        new ActivityTestRule<>(MainActivity.class);
    
    @Test
    public void testPluginButton() {
        onView(withText("Plugin Button"))
            .check(matches(isDisplayed()));
    }
}

iOS XCUITest

// example/ios/RunnerUITests/RunnerUITests.m
#import <XCTest/XCTest.h>

@interface RunnerUITests : XCTestCase
@end

@implementation RunnerUITests

- (void)setUp {
    [super setUp];
    XCUIApplication *app = [[XCUIApplication alloc] init];
    [app launch];
}

- (void)testPluginButtonExists {
    XCUIApplication *app = [[XCUIApplication alloc] init];
    XCUIElement *button = app.buttons[@"Plugin Button"];
    
    XCTAssertTrue(button.exists);
}

- (void)testPluginButtonClick {
    XCUIApplication *app = [[XCUIApplication alloc] init];
    XCUIElement *button = app.buttons[@"Plugin Button"];
    
    [button tap];
    
    XCUIElement *result = app.staticTexts[@"Success"];
    XCTAssertTrue(result.exists);
}

@end

Testing Plugin Initialization

Test Plugin Registration

testWidgets('plugin registers correctly', (tester) async {
  final calls = <MethodCall>[];
  
  TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
      .setMockMethodCallHandler(
    const MethodChannel('com.example.my_plugin'),
    (MethodCall methodCall) async {
      calls.add(methodCall);
      return null;
    },
  );
  
  await tester.pumpWidget(const MyApp());
  
  expect(calls.any((call) => call.method == 'initialize'), true);
});

Testing Error Handling

Native Error Handling

testWidgets('handles native errors', (tester) async {
  TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
      .setMockMethodCallHandler(
    const MethodChannel('com.example.my_plugin'),
    (MethodCall methodCall) async {
      if (methodCall.method == 'failingMethod') {
        throw PlatformException(
          code: 'ERROR',
          message: 'Native error occurred',
        );
      }
      return null;
    },
  );
  
  final plugin = MyPlugin();
  
  expect(
    () => plugin.failingMethod(),
    throwsA(isA<PlatformException>()),
  );
});

Testing Platform-Specific Features

Android-Specific Tests

@Test
public void testAndroidOnlyFeature() {
    MyPlugin plugin = new MyPlugin();
    
    // Test Android-specific API
    Context context = InstrumentationRegistry.getTargetContext();
    String deviceModel = plugin.getDeviceModel(context);
    
    assertNotNull(deviceModel);
}

iOS-Specific Tests

- (void)testIOSOnlyFeature {
    MyPlugin *plugin = [[MyPlugin alloc] init];
    
    // Test iOS-specific API
    NSString *deviceName = plugin.getDeviceName();
    
    XCTAssertNotNil(deviceName);
}

Testing Multiple Platforms

Platform Detection Test

testWidgets('works on all platforms', (tester) async {
  final plugin = MyPlugin();
  final platform = await plugin.getPlatform();
  
  if (Platform.isAndroid) {
    expect(platform, contains('Android'));
  } else if (Platform.isIOS) {
    expect(platform, contains('iOS'));
  }
});

Testing Performance

Native Performance Test

@Test
public void testPerformance() {
    MyPlugin plugin = new MyPlugin();
    
    long startTime = System.nanoTime();
    plugin.heavyOperation();
    long endTime = System.nanoTime();
    
    long duration = endTime - startTime;
    long maxDuration = 100_000_000; // 100ms
    
    assertTrue("Operation too slow", duration < maxDuration);
}

Best Practices

  1. Test at each layer - Dart, native, and integration
  2. Mock platform channels - In Dart unit tests
  3. Test error cases - Native failures, network issues
  4. Use integration tests - Test full plugin functionality
  5. Test on real devices - For platform-specific features
  6. Keep tests isolated - Each test should be independent
  7. Test all platforms - Android, iOS, web, desktop
  8. Document platform limitations - Note platform-specific behavior

CI/CD for Plugins

GitHub Actions

name: Plugin Tests

on: [push, pull_request]

jobs:
  dart-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: subosito/flutter-action@v2
      - run: flutter test
      
  android-tests:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v2
      - name: Run Android tests
        run: |
          cd example/android
          ./gradlew testDebugUnitTest
          
  ios-tests:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v2
      - name: Run iOS tests
        run: |
          cd example/ios
          xcodebuild test -workspace Runner.xcworkspace \
            -scheme Runner -configuration Debug