chore: bring flutter client, docs, deploy and tools into monorepo
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,493 @@
|
||||
# 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
|
||||
|
||||
```dart
|
||||
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
|
||||
|
||||
```dart
|
||||
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`:
|
||||
|
||||
```yaml
|
||||
dev_dependencies:
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
my_plugin:
|
||||
path: ../
|
||||
```
|
||||
|
||||
### Basic Integration Test
|
||||
|
||||
```dart
|
||||
// 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
|
||||
|
||||
```dart
|
||||
// test_driver/integration_test.dart
|
||||
import 'package:integration_test/integration_test_driver.dart';
|
||||
|
||||
Future<void> main() => integrationDriver();
|
||||
```
|
||||
|
||||
### Run Integration Tests
|
||||
|
||||
```bash
|
||||
# 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`:
|
||||
|
||||
```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
|
||||
|
||||
```bash
|
||||
# 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`:
|
||||
|
||||
```objectivec
|
||||
#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
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```java
|
||||
// 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
|
||||
|
||||
```objectivec
|
||||
// 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
|
||||
|
||||
```dart
|
||||
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
|
||||
|
||||
```dart
|
||||
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
|
||||
|
||||
```java
|
||||
@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
|
||||
|
||||
```objectivec
|
||||
- (void)testIOSOnlyFeature {
|
||||
MyPlugin *plugin = [[MyPlugin alloc] init];
|
||||
|
||||
// Test iOS-specific API
|
||||
NSString *deviceName = plugin.getDeviceName();
|
||||
|
||||
XCTAssertNotNil(deviceName);
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Multiple Platforms
|
||||
|
||||
### Platform Detection Test
|
||||
|
||||
```dart
|
||||
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
|
||||
|
||||
```java
|
||||
@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
|
||||
|
||||
```yaml
|
||||
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
|
||||
```
|
||||
Reference in New Issue
Block a user