
Flutter Testing
- 1.4k installs
- 105 repo stars
- Updated May 1, 2026
- madteacher/mad-agents-skills
flutter-testing is an agent skill that apply flutter-testing agent skill workflows from documented skill.md guidance.
About
flutter-testing is an agent skill from madteacher/mad-agents-skills that apply flutter-testing agent skill workflows from documented skill.md guidance. # Flutter Testing You are a Flutter testing engineer for app, package, and plugin projects. ## Principle 0 Do not write Flutter tests from memory. First inspect the project, choose the right test layer, read the routed reference for the scenario, then run the closest validation command. Broken or flaky tests waste more time than missing tests be Developers invoke flutter-testing during ship/testing work for testing & qa tasks. The skill documents triggers, prerequisites, and step-by-step workflows grounded in SKILL.md. Compatible with Claude Code, Cursor, and Codex agent runtimes that load marketplace skills. Review the Security Audits panel on this listing before installing in production environments.
- You are a Flutter testing engineer for app, package, and plugin projects.
- Do not write Flutter tests from memory. First inspect the project, choose the
- right test layer, read the routed reference for the scenario, then run the
- closest validation command. Broken or flaky tests waste more time than missing
- tests because they create false confidence and slow future changes.
Flutter Testing by the numbers
- 1,417 all-time installs (skills.sh)
- +11 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #488 of 2,159 Testing & QA skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 29, 2026 (Skillselion catalog sync)
flutter-testing capabilities & compatibility
- Capabilities
- you are a flutter testing engineer for app, pack · do not write flutter tests from memory. first in · right test layer, read the routed reference for · closest validation command. broken or flaky test · tests because they create false confidence and s
- Use cases
- orchestration
What flutter-testing says it does
You are a Flutter testing engineer for app, package, and plugin projects.
Do not write Flutter tests from memory. First inspect the project, choose the
right test layer, read the routed reference for the scenario, then run the
npx skills add https://github.com/madteacher/mad-agents-skills --skill flutter-testingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.4k |
|---|---|
| repo stars | ★ 105 |
| Security audit | 3 / 3 scanners passed |
| Last updated | May 1, 2026 |
| Repository | madteacher/mad-agents-skills ↗ |
What it does
Apply flutter-testing agent skill workflows from documented SKILL.md guidance.
Who is it for?
Developers working on testing & qa during ship tasks.
Skip if: Tasks outside Testing & QA scope described in SKILL.md.
When should I use this skill?
Apply flutter-testing agent skill workflows from documented SKILL.md guidance.
What you get
Completed testing & qa workflow aligned with SKILL.md steps.
- corrected widget layout code
- passing widget test suite
Files
Flutter Testing
You are a Flutter testing engineer for app, package, and plugin projects.
Principle 0
Do not write Flutter tests from memory. First inspect the project, choose the right test layer, read the routed reference for the scenario, then run the closest validation command. Broken or flaky tests waste more time than missing tests because they create false confidence and slow future changes.
Workflow
1. Inspect the project before changing tests: pubspec.yaml, existing test/, integration_test/, test_driver/, generated mock files, state management, platform abstractions, plugin usage, and CI commands. 2. Choose the test layer:
- Pure Dart functions, repositories, services, state reducers, and view
models: unit tests.
- Single widgets, forms, navigation shells, semantics, gestures, responsive
layout, and UI state: widget tests.
- Complete user flows, real device behavior, screenshots, performance
reports, native/plugin bridges, and browser/device targets: integration tests.
- Flutter plugins or app code using platform channels: plugin and mocking
guidance. 3. Read only the reference files needed for the chosen scenario. 4. Implement the smallest deterministic test or test fix using the project's existing test style, dependency injection pattern, keys, fixtures, mocks, and CI constraints. 5. Run mandatory validation. If validation cannot run, report the exact blocker and the concrete risk instead of presenting the change as verified.
Resource Routing
| Task | Read or run | Why |
|---|---|---|
| Write or fix pure Dart tests, async tests, stream tests, matchers, exceptions, or test organization | references/unit-testing.md | Unit-test patterns that compile under Dart null safety |
| Write or fix widget tests, finders, gestures, forms, navigation, semantics, scrolling, animations, or layout-size tests | references/widget-testing.md | flutter_test APIs and widget-specific pitfalls |
| Add or fix integration tests, device/browser runs, performance reports, screenshots, persistence flows, platform scenarios, or CI integration | references/integration-testing.md | Current integration_test APIs and target commands |
| Mock dependencies, repositories, platform channels, generated Mockito mocks, manual fakes, or state-management collaborators | references/mocking.md | Deterministic test-double patterns and mock generation |
Diagnose failing tests, layout errors, MissingPluginException, finder failures, timeouts, async hangs, or debugging output | references/common-errors.md | Error-to-fix mapping without guessing |
| Test Flutter plugin packages, native Android/iOS code, example-app integration tests, or plugin registration/error paths | references/plugin-testing.md | Plugin package layout and native/Dart test split |
| Edit this skill, references, or examples | scripts/verify-examples.sh | Deterministic smoke check for stale patterns and broken links |
Mandatory Validation
- After editing Dart tests or production code, run the narrowest relevant
command first, such as flutter test test/my_widget_test.dart, flutter test --plain-name "subtree", or dart test for pure Dart packages.
- After adding or changing generated Mockito mocks, run
dart run build_runner build or the repository's established build command before running tests.
- For integration tests on mobile or desktop targets, run `flutter test -d
<device-id> integration_test/<test_file>.dart` when a device target is required; otherwise run the documented project command.
- For browser integration tests that require
integrationDriver, run
flutter drive --driver=test_driver/integration_test.dart --target=integration_test/<test_file>.dart -d chrome or the project's web driver command.
- After fixing flaky tests, run the specific test more than once or use the
repository's repeat/randomization option if one exists.
- After editing this skill or its references, run
bash flutter-testing/scripts/verify-examples.sh.
Constraints
- Prefer
package:flutter_test/flutter_test.dartfor widget and integration
tests, and package:test/test.dart only for pure Dart tests that do not need Flutter bindings.
- Test user-visible behavior and public contracts. Do not assert private method
calls, widget internals, or incidental rebuild counts unless performance work explicitly requires it.
- Prefer dependency injection, fake repositories, fake platform interfaces, or
MethodChannel handlers over real network, real storage, timers, permissions, or OS dialogs.
- Do not use fixed
Future.delayedwaits to hide async uncertainty. Use
deterministic fakes, explicit pumps, pumpAndSettle only when animations can settle, or bounded custom pumps.
- Do not use obsolete commands or APIs:
--no-sound-null-safety,
flutter test --platform ..., tester.trace, tester.takeScreenshot, captureNamed, or flutter pub run build_runner build.
- Do not simulate connectivity, permissions, or persistence by changing the
test surface size or by re-pumping the app with hidden state. Use explicit fakes, test hooks, or real integration targets.
Fallback
If the repository lacks enough context to choose the test layer, ask for the target behavior and preferred test level. If devices, browsers, native tooling, network access, code generation, or dependency downloads block validation, finish with the exact command that failed, what was not verified, and the risk left for the user.
Common Testing Errors
Overview
This guide covers frequently encountered Flutter testing errors and their solutions.
Layout Errors
'A RenderFlex overflowed...'
Error Message:
The following assertion was thrown during layout:
A RenderFlex overflowed by 1146 pixels on the right.Cause: Yellow and black stripes indicate overflow when a child widget is too large for its parent (Row/Column).
Solution: Wrap the overflowing widget in Expanded or Flexible.
// Problem
Row(
children: [
Icon(Icons.message),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Title', style: Theme.of(context).textTheme.headlineMedium),
Text('Very long text that overflows the available space...'),
],
),
],
)
// Solution
Row(
children: [
Icon(Icons.message),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Title', style: Theme.of(context).textTheme.headlineMedium),
Text('Very long text that fits in the available space...'),
],
),
),
],
)'Vertical viewport was given unbounded height'
Error Message:
Vertical viewport was given unbounded height.
Viewports expand in the scrolling direction to fill their container.
This situation typically happens when a scrollable widget is nested inside another scrollable widget.Cause: ListView or other scrollable widget inside Column without height constraints.
Solution: Wrap in Expanded or use shrinkWrap: true.
// Problem
Column(
children: [
Text('Header'),
ListView(
children: [
ListTile(leading: Icon(Icons.map), title: Text('Map')),
ListTile(leading: Icon(Icons.subway), title: Text('Subway')),
],
),
],
)
// Solution 1: Expanded
Column(
children: [
Text('Header'),
Expanded(
child: ListView(
children: [
ListTile(leading: Icon(Icons.map), title: Text('Map')),
ListTile(leading: Icon(Icons.subway), title: Text('Subway')),
],
),
),
],
)
// Solution 2: shrinkWrap
Column(
children: [
Text('Header'),
ListView(
shrinkWrap: true,
children: [
ListTile(leading: Icon(Icons.map), title: Text('Map')),
ListTile(leading: Icon(Icons.subway), title: Text('Subway')),
],
),
],
)'An InputDecorator...cannot have an unbounded width'
Error Message:
An InputDecorator, which is typically created by a TextField, cannot have an unbounded width.
This happens when the parent widget does not provide a finite width constraint.Cause: TextField or TextFormField inside Row without width constraints.
Solution: Wrap in Expanded or SizedBox.
// Problem
Row(
children: [TextField()],
)
// Solution 1: Expanded
Row(
children: [Expanded(child: TextFormField())],
)
// Solution 2: SizedBox
Row(
children: [SizedBox(width: 200, child: TextFormField())],
)Widget Lifecycle Errors
'setState called during build'
Error Message:
setState() or markNeedsBuild() called during build.
This Overlay widget cannot be marked as needing to build because the framework
is already in the process of building widgets.Cause: Calling setState (or methods that call it like showDialog) directly in the build method.
Solution: Use WidgetsBinding.instance.addPostFrameCallback or Navigator API.
// Problem
Widget build(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(title: Text('Error!')),
);
return Center(child: Text('Show Dialog'));
}
// Solution 1: Post-frame callback
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showDialog(
context: context,
builder: (context) => AlertDialog(title: Text('Success!')),
);
});
return Center(child: Text('Show Dialog'));
}
// Solution 2: Navigator API (for initial navigation)
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (_, __, ___) => DialogScreen(),
),
);
},
child: Text('Show Dialog'),
);
}Parent Widget Errors
'Incorrect use of ParentData widget'
Error Message:
The following assertion was thrown while looking for parent data:
Incorrect use of ParentDataWidget.
Usually, this indicates that at least one of the offending ParentDataWidgets
listed above is not placed directly inside a compatible ancestor widget.Cause: Using widgets that require specific parent widgets in wrong context.
Common Solutions:
| Widget | Expected Parent | Solution |
|---|---|---|
Flexible | Row, Column, Flex | Ensure parent is Row/Column/Flex |
Expanded | Row, Column, Flex | Ensure parent is Row/Column/Flex |
Positioned | Stack | Wrap in Stack |
TableCell | Table | Ensure parent is Table |
// Problem
Column(
children: [
Expanded(child: Text('Wrong!')), // Expanded needs Row/Column parent, not Column children
],
)
// Solution
Column(
children: [
Row(
children: [
Expanded(child: Text('Correct!')),
],
),
],
)Testing-Specific Errors
'WidgetTester.pumpWidget() called with a widget that doesn't include a MaterialApp'
Error Message:
WidgetTester.pumpWidget() called with a widget that doesn't include a MaterialApp.Cause: Pumping widget without MaterialApp context.
Solution: Wrap widget in MaterialApp.
// Problem
testWidgets('test without MaterialApp', (tester) async {
await tester.pumpWidget(MyWidget());
});
// Solution
testWidgets('test with MaterialApp', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: MyWidget(),
),
);
});'No Finder found'
Error Message:
No Finder found.
Test failed: No elements found matching the finder.Cause: Widget not found in tree at time of test.
Common Solutions:
// 1. Wrong text/content
expect(find.text('Wrong Text'), findsNothing); // Error
// Solution: Use correct text
expect(find.text('Correct Text'), findsOneWidget);
// 2. Widget not built yet
testWidgets('widget not built', (tester) async {
await tester.pumpWidget(MyWidget());
expect(find.text('Async Text'), findsOneWidget); // Error
});
// Solution: Pump and settle for async operations
testWidgets('widget not built', (tester) async {
await tester.pumpWidget(MyWidget());
await tester.pumpAndSettle(); // Wait for async operations
expect(find.text('Async Text'), findsOneWidget);
});
// 3. Wrong finder type
testWidgets('wrong finder', (tester) async {
await tester.pumpWidget(TextButton(child: Text('Button')));
await tester.tap(find.byType(ElevatedButton)); // Error
});
// Solution: Use correct type or use Text finder
testWidgets('correct finder', (tester) async {
await tester.pumpWidget(TextButton(child: Text('Button')));
await tester.tap(find.byType(TextButton)); // Correct
});Plugin/Platform Errors
'MissingPluginException'
Error Message:
MissingPluginException(No implementation found for method MethodName on channel channel.name)Cause: Plugin not registered or mock not set up in tests.
Solution: Mock the method channel in tests.
import 'package:flutter/services.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('your.plugin.channel'),
(MethodCall methodCall) async {
if (methodCall.method == 'getPlatformVersion') {
return 'Android 12';
}
throw MissingPluginException();
},
);
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('your.plugin.channel'),
null,
);
});
}Async Errors
'TimeoutException'
Error Message:
TimeoutException after 0:00:05.000000: Test timed out after 5 seconds.Cause: Async operation not completing or pumpAndSettle() waiting indefinitely.
Solution: Set timeout or fix infinite loops.
// Solution 1: Increase timeout
testWidgets(
'long-running test',
(tester) async {
await tester.pumpWidget(MyWidget());
await Future.delayed(const Duration(seconds: 10));
},
timeout: const Timeout(Duration(minutes: 1)),
);
// Solution 2: Fix infinite pumpAndSettle
testWidgets('fix infinite settle', (tester) async {
await tester.pumpWidget(MyWidget());
await tester.pump(); // Use pump() instead of pumpAndSettle() if animation loops
});Data Errors
'RangeError'
Error Message:
RangeError: Index out of range: index should be less than 5, but is 5Cause: Accessing list/array with invalid index.
Solution: Check bounds before access or handle empty cases.
// Problem
testWidgets('range error', (tester) async {
final items = [1, 2, 3, 4, 5];
expect(items[5], 6); // Error: index 5 out of range
});
// Solution 1: Check bounds
testWidgets('safe access', (tester) async {
final items = [1, 2, 3, 4, 5];
if (items.length > 5) {
expect(items[5], 6);
}
});
// Solution 2: Handle empty lists
testWidgets('handle empty list', (tester) async {
final items = <int>[];
if (items.isEmpty) {
expect(find.text('No items'), findsOneWidget);
} else {
expect(items.first, 1);
}
});Debugging Tips
Enable Verbose Logging
flutter test --verboseTake Screenshots in Integration Tests
import 'package:integration_test/integration_test.dart';
testWidgets('debug with screenshot', (tester) async {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
as IntegrationTestWidgetsFlutterBinding;
await tester.pumpWidget(MyWidget());
await binding.convertFlutterSurfaceToImage();
await tester.pump();
await binding.takeScreenshot('debug-state');
});Print Widget Tree
testWidgets('print tree', (tester) async {
await tester.pumpWidget(MyWidget());
debugDumpApp();
});Use Debug Flags
testWidgets('debug flags', (tester) async {
debugPrint('Current state: ...');
await tester.pumpWidget(MyWidget());
debugPrint('Widget tree: ${tester.widgetList(find.byType(MyWidget))}');
});Prevention
Use Type Safety
// Good: Use type-safe access
final text = tester.widget<Text>(find.byType(Text));
// Avoid: Unsafe casts
final text = find.byType(Text).evaluate().first.widget as Text;Check Before Actions
testWidgets('safe tap', (tester) async {
await tester.pumpWidget(MyWidget());
final button = find.byType(ElevatedButton);
if (button.evaluate().isNotEmpty) {
await tester.tap(button);
} else {
fail('Button not found');
}
});Handle All Cases
testWidgets('comprehensive', (tester) async {
await tester.pumpWidget(MyWidget());
// Check all possible states
if (find.text('Loading').evaluate().isNotEmpty) {
// Handle loading state
} else if (find.text('Error').evaluate().isNotEmpty) {
// Handle error state
} else {
// Handle success state
}
});Integration Testing Guide
Overview
Integration tests test complete apps or large parts of apps on real devices or emulators. They verify that all widgets and services work together as expected and can measure performance.
When to Write Integration Tests
- Testing complete user flows across multiple screens
- Verifying navigation between pages
- Testing state persistence
- Validating real device behavior (sensors, camera, etc.)
- Performance profiling and benchmarking
- Testing platform-specific features
Setup
Add Dependency
Add to pubspec.yaml:
dev_dependencies:
integration_test:
sdk: flutter
flutter_test:
sdk: flutterCreate Test Driver
Create test_driver/integration_test.dart when using flutter drive, browser integration tests, screenshots, or response data from performance traces:
import 'package:integration_test/integration_test_driver.dart';
Future<void> main() => integrationDriver();Test Structure
Basic Integration Test
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('app launches and displays home', (tester) async {
await tester.pumpWidget(const MyApp());
expect(find.text('Home Screen'), findsOneWidget);
});
}Running Integration Tests
# Run on all devices
flutter test integration_test/
# Run on specific device
flutter test -d <device-id> integration_test/my_test.dart
# Run specific test file
flutter test integration_test/my_test.dart
# Run a browser/driver integration test
flutter drive \
--driver=test_driver/integration_test.dart \
--target=integration_test/my_test.dart \
-d chromeTesting User Flows
Multi-Screen Navigation
testWidgets('complete user flow from login to dashboard', (tester) async {
await tester.pumpWidget(const MyApp());
// Login screen
expect(find.text('Login'), findsOneWidget);
// Enter credentials
await tester.enterText(find.byKey(const Key('username')), 'user@example.com');
await tester.enterText(find.byKey(const Key('password')), 'password123');
// Tap login
await tester.tap(find.byKey(const Key('login-button')));
await tester.pumpAndSettle();
// Dashboard screen
expect(find.text('Dashboard'), findsOneWidget);
expect(find.text('Welcome, user@example.com'), findsOneWidget);
});Tab Navigation
testWidgets('navigate between tabs', (tester) async {
await tester.pumpWidget(const MyApp());
// Initial tab
expect(find.text('Home'), findsOneWidget);
// Tap second tab
await tester.tap(find.text('Profile'));
await tester.pumpAndSettle();
expect(find.text('Home'), findsNothing);
expect(find.text('Profile'), findsOneWidget);
// Navigate back
await tester.tap(find.text('Home'));
await tester.pumpAndSettle();
expect(find.text('Home'), findsOneWidget);
});Testing Forms
Complete Form Submission
testWidgets('submit registration form', (tester) async {
await tester.pumpWidget(const MyApp());
// Navigate to registration
await tester.tap(find.text('Sign Up'));
await tester.pumpAndSettle();
// Fill form
await tester.enterText(find.byKey(const Key('name')), 'John Doe');
await tester.enterText(find.byKey(const Key('email')), 'john@example.com');
await tester.enterText(find.byKey(const Key('password')), 'password123');
await tester.enterText(find.byKey(const Key('confirm-password')), 'password123');
// Submit
await tester.tap(find.byKey(const Key('submit')));
await tester.pumpAndSettle();
// Verify success
expect(find.text('Registration Successful'), findsOneWidget);
});Form Validation
testWidgets('form shows validation errors', (tester) async {
await tester.pumpWidget(const MyApp());
await tester.tap(find.text('Sign Up'));
await tester.pumpAndSettle();
// Submit empty form
await tester.tap(find.byKey(const Key('submit')));
await tester.pump();
// Verify errors
expect(find.text('Name is required'), findsOneWidget);
expect(find.text('Email is required'), findsOneWidget);
expect(find.text('Password is required'), findsOneWidget);
// Fill with invalid email
await tester.enterText(find.byKey(const Key('email')), 'invalid-email');
await tester.tap(find.byKey(const Key('submit')));
await tester.pump();
expect(find.text('Invalid email format'), findsOneWidget);
});Testing Lists and Data
Loading and Displaying Data
testWidgets('load and display items from API', (tester) async {
await tester.pumpWidget(const MyApp());
// Wait for data load
await tester.pumpAndSettle();
// Verify items loaded
expect(find.byType(ListTile), findsWidgets);
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 2'), findsOneWidget);
});Pull-to-Refresh
testWidgets('pull to refresh updates list', (tester) async {
await tester.pumpWidget(const MyApp());
await tester.pumpAndSettle();
// Pull down
await tester.drag(
find.byType(RefreshIndicator),
const Offset(0, 300),
);
await tester.pumpAndSettle();
// Verify refreshed data
expect(find.text('Updated Item 1'), findsOneWidget);
});Infinite Scrolling
testWidgets('load more items on scroll', (tester) async {
await tester.pumpWidget(const MyApp());
await tester.pumpAndSettle();
// Initial items
expect(find.text('Item 20'), findsNothing);
// Scroll to bottom to trigger load more
await tester.fling(
find.byType(ListView),
const Offset(0, -2000),
10000,
);
await tester.pumpAndSettle();
// New items loaded
expect(find.text('Item 20'), findsOneWidget);
});Testing State Persistence
Persisted Settings
testWidgets('user preference persists across app rebuild', (tester) async {
final preferences = FakePreferencesStore();
await tester.pumpWidget(MyApp(preferences: preferences));
// Change setting
await tester.tap(find.text('Dark Mode'));
await tester.pumpAndSettle();
// Rebuild the app with the same injected storage.
await tester.pumpWidget(const SizedBox.shrink());
await tester.pumpWidget(MyApp(preferences: preferences));
await tester.pumpAndSettle();
// Verify setting persisted
expect(find.text('Light Mode'), findsNothing);
expect(find.text('Dark Mode'), findsOneWidget);
});Authentication State
testWidgets('user stays logged in', (tester) async {
final authStore = FakeAuthStore();
await tester.pumpWidget(MyApp(authStore: authStore));
// Login
await tester.enterText(find.byKey(const Key('email')), 'user@example.com');
await tester.enterText(find.byKey(const Key('password')), 'password');
await tester.tap(find.byKey(const Key('login')));
await tester.pumpAndSettle();
expect(find.text('Dashboard'), findsOneWidget);
// Rebuild the app with the same injected auth store.
await tester.pumpWidget(const SizedBox.shrink());
await tester.pumpWidget(MyApp(authStore: authStore));
await tester.pumpAndSettle();
// Still logged in
expect(find.text('Dashboard'), findsOneWidget);
expect(find.text('Login'), findsNothing);
});Performance Testing
Measuring Frame Time
testWidgets('measure scrolling performance', (tester) async {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
as IntegrationTestWidgetsFlutterBinding;
await tester.pumpWidget(const MyApp());
await tester.pumpAndSettle();
await binding.watchPerformance(() async {
await tester.fling(
find.byType(ListView),
const Offset(0, -1000),
5000,
);
await tester.pumpAndSettle();
}, reportKey: 'scrolling_performance');
});Tracking Build Count
testWidgets('widget doesn't rebuild unnecessarily', (tester) async {
int buildCount = 0;
await tester.pumpWidget(
MaterialApp(
home: Builder(
builder: (context) {
buildCount++;
return const MyWidget();
},
),
),
);
expect(buildCount, 1);
// Perform unrelated action
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
// Should not rebuild
expect(buildCount, 1);
});Memory Tracking on Native Targets
import 'dart:io' show ProcessInfo;
testWidgets('monitor memory usage during scrolling', (tester) async {
await tester.pumpWidget(const MyApp());
final initialMemory = ProcessInfo.currentRss;
await tester.fling(
find.byType(ListView),
const Offset(0, -5000),
10000,
);
await tester.pumpAndSettle();
final finalMemory = ProcessInfo.currentRss;
final memoryIncrease = finalMemory - initialMemory;
// Memory increase should be reasonable (< 10MB)
expect(memoryIncrease, lessThan(10 * 1024 * 1024));
});Testing Platform-Specific Features
Permissions
testWidgets('request and handle camera permission', (tester) async {
final permissions = FakePermissionGateway(
camera: PermissionStatus.granted,
);
await tester.pumpWidget(MyApp(permissions: permissions));
// Request camera access
await tester.tap(find.text('Take Photo'));
await tester.pumpAndSettle();
// Verify camera screen appears
expect(find.byType(CameraPreview), findsOneWidget);
});Deep Links
testWidgets('handle deep link to product page', (tester) async {
await tester.pumpWidget(const MyApp(initialRoute: '/product/123'));
await tester.pumpAndSettle();
expect(find.text('Product #123'), findsOneWidget);
});In-App Purchases
testWidgets('complete purchase flow', (tester) async {
await tester.pumpWidget(const MyApp());
await tester.tap(find.text('Premium'));
await tester.pumpAndSettle();
await tester.tap(find.text('Subscribe'));
await tester.pumpAndSettle();
// Verify purchase completed
expect(find.text('Premium Active'), findsOneWidget);
});Testing Network
Offline Behavior
testWidgets('handle offline state gracefully', (tester) async {
final connectivity = FakeConnectivity(isOnline: false);
final api = FakeApiClient(networkError: const SocketException('offline'));
await tester.pumpWidget(MyApp(connectivity: connectivity, apiClient: api));
await tester.pumpAndSettle();
// Verify offline message
expect(find.text('No internet connection'), findsOneWidget);
// Simulate back online
connectivity.isOnline = true;
api.networkError = null;
await tester.tap(find.byKey(const Key('retry-button')));
await tester.pumpAndSettle();
// Reload data
expect(find.text('Data loaded'), findsOneWidget);
});Error Handling
testWidgets('handle network errors', (tester) async {
await tester.pumpWidget(const MyApp());
// Trigger API call
await tester.tap(find.text('Load Data'));
await tester.pumpAndSettle();
// Verify error message displayed
expect(find.text('Failed to load data'), findsOneWidget);
expect(find.byKey(const Key('retry-button')), findsOneWidget);
});Testing Animations
Smooth Animations
testWidgets('animation runs smoothly', (tester) async {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
as IntegrationTestWidgetsFlutterBinding;
await tester.pumpWidget(const MyApp());
await binding.traceAction(() async {
await tester.tap(find.byKey(const Key('animate')));
await tester.pumpAndSettle();
}, reportKey: 'animation_timeline');
});Best Practices
1. Test critical user paths - Focus on important flows 2. Keep tests independent - Each test should be standalone 3. Use descriptive names - Explain what the test does 4. Wait for async operations - Use pumpAndSettle() 5. Test real behavior - Verify actual device behavior, not mocks 6. Measure performance - Track frame times, memory, and build counts 7. Test edge cases - Offline, errors, empty states 8. Keep tests fast - Avoid unnecessary delays
Debugging Integration Tests
Take Screenshots
testWidgets('debug test with screenshot', (tester) async {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
as IntegrationTestWidgetsFlutterBinding;
await tester.pumpWidget(const MyApp());
// On Android, call convertFlutterSurfaceToImage and pump before screenshots.
await binding.convertFlutterSurfaceToImage();
await tester.pump();
await binding.takeScreenshot('test-screenshot');
// Perform actions
await tester.tap(find.text('Button'));
await tester.pumpAndSettle();
// Take another screenshot
await binding.takeScreenshot('after-tap');
});Print Widget Tree
testWidgets('print widget tree for debugging', (tester) async {
await tester.pumpWidget(const MyApp());
// Print widget tree
debugDumpApp();
});CI/CD Integration
GitHub Actions
name: Integration Tests
on: [push, pull_request]
jobs:
test:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.19.0'
- name: Get dependencies
run: flutter pub get
- name: Run integration tests
run: flutter test integration_test/Test Reports
# Generate test report
flutter test integration_test/ --reporter expanded
# Generate JSON report
flutter test integration_test/ --reporter json > test-results.jsonMocking Guide
Overview
Mocking replaces real dependencies with test doubles to isolate code and make tests deterministic. This guide covers mocking strategies for Flutter tests.
When to Mock
- Testing code that depends on external services (APIs, databases)
- Isolating components for unit tests
- Simulating error conditions and edge cases
- Making tests faster and more reliable
- Testing code that uses plugins
Using Mockito
Setup
Add to pubspec.yaml:
dev_dependencies:
build_runner: ^2.10.4
mockito: ^5.6.1Generate Mocks
Create a test file with a Mockito annotation and import the generated file:
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'user_repository_test.mocks.dart';
@GenerateMocks([ApiClient, UserRepository])
void main() {}Generate mocks:
dart run build_runner buildBasic Mocking
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'my_service_test.mocks.dart';
@GenerateMocks([ApiService])
void main() {
late MockApiService mockApiService;
late MyService myService;
setUp(() {
mockApiService = MockApiService();
myService = MyService(mockApiService);
});
test('calls API and returns data', () async {
// Arrange
when(mockApiService.fetchData())
.thenAnswer((_) async => {'data': 'value'});
// Act
final result = await myService.getData();
// Assert
expect(result['data'], 'value');
verify(mockApiService.fetchData()).called(1);
});
}Mocking Different Types
Mocking Classes
class DataService {
Future<String> fetchData() async => 'real data';
}
// In a real test file, generate MockDataService with @GenerateMocks([DataService]).
test('mocks class method', () async {
final mockService = MockDataService();
when(mockService.fetchData())
.thenAnswer((_) async => 'mocked data');
final result = await mockService.fetchData();
expect(result, 'mocked data');
});Mocking Interfaces
abstract class Storage {
void save(String key, String value);
String? load(String key);
}
class FakeStorage implements Storage {
final values = <String, String>{};
@override
void save(String key, String value) {
values[key] = value;
}
@override
String? load(String key) => values[key];
}
// Test
test('uses a fake storage implementation', () {
final storage = FakeStorage();
storage.save('key', 'value');
expect(storage.load('key'), 'value');
});Mocking Streams
class StreamService {
Stream<int> get counterStream => StreamController<int>().stream;
}
// Test
test('mocks stream', () async {
final mockService = MockStreamService();
final controller = StreamController<int>();
when(mockService.counterStream).thenAnswer((_) => controller.stream);
expectLater(
mockService.counterStream,
emitsInOrder([1, 2, 3]),
);
controller.add(1);
controller.add(2);
controller.add(3);
await controller.close();
});Mockito Features
Returning Values
when(mockService.getData()).thenReturn('value');
when(mockService.getUser(id)).thenReturn(User(id: id, name: 'Test'));
when(mockService.getList()).thenReturn([1, 2, 3]);Throwing Errors
when(mockService.getData())
.thenThrow(Exception('Network error'));
when(mockService.saveData(data))
.thenThrow(FormatException('Invalid data'));Async Responses
when(mockService.fetchData())
.thenAnswer((_) async => 'async data');
when(mockService.loadUser(id))
.thenAnswer((_) async => Future.delayed(
const Duration(milliseconds: 100),
() => User(id: id),
));Sequential Returns
when(mockService.getData())
.thenReturn('first')
.thenReturn('second')
.thenThrow(Exception('error'));
// First call returns 'first'
expect(await mockService.getData(), 'first');
// Second call returns 'second'
expect(await mockService.getData(), 'second');
// Third call throws
await expectLater(mockService.getData(), throwsException);Conditional Returns
when(mockService.getUser(argThat(allOf(isA<String>(), contains('@')))))
.thenReturn(User(email: 'test@example.com'));
when(mockService.getUser(argThat(predicate<String>((arg) => arg.length > 5))))
.thenReturn(User(name: 'Long Name'));
// Matches email format
expect(mockService.getUser('test@example.com').email, 'test@example.com');
// Matches length > 5
expect(mockService.getUser('Long Name').name, 'Long Name');Capturing Arguments
mockService.save('key1', 'value1');
mockService.save('key2', 'value2');
final captured = verify(mockService.save(captureAny, captureAny)).captured;
expect(captured, ['key1', 'value1', 'key2', 'value2']);Verification
Verify Calls
mockService.getData();
verify(mockService.getData()).called(1);
verify(mockService.getData()).called(greaterThan(0));
verifyNever(mockService.otherMethod());Verify In Order
mockService.first();
mockService.second();
mockService.third();
verifyInOrder([
mockService.first(),
mockService.second(),
mockService.third(),
]);Verify Specific Arguments
mockService.save('key', 'value');
verify(mockService.save('key', 'value')).called(1);
verify(mockService.save(argThat(isA<String>()), any)).called(1);Verify No More Interactions
mockService.getData();
verify(mockService.getData()).called(1);
verifyNoMoreInteractions(mockService);
verifyNoInteractions(otherMockService);Mocking Platform Channels
Basic Platform Channel Mock
import 'package:flutter/services.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('com.example.app/channel'),
(MethodCall methodCall) async {
switch (methodCall.method) {
case 'getBatteryLevel':
return 85;
case 'openUrl':
return true;
default:
throw MissingPluginException();
}
},
);
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('com.example.app/channel'),
null,
);
});
test('gets battery level', () async {
final platform = MethodChannel('com.example.app/channel');
final result = await platform.invokeMethod('getBatteryLevel');
expect(result, 85);
});
}Platform Channel with Arguments
setUp(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('com.example.app/storage'),
(MethodCall methodCall) async {
if (methodCall.method == 'save') {
final args = methodCall.arguments as Map;
storage[args['key']] = args['value'];
return true;
} else if (methodCall.method == 'load') {
final args = methodCall.arguments as Map;
return storage[args['key']];
}
return null;
},
);
});
test('saves and loads from platform storage', () async {
final platform = MethodChannel('com.example.app/storage');
await platform.invokeMethod('save', {'key': 'test', 'value': 'data'});
final result = await platform.invokeMethod('load', {'key': 'test'});
expect(result, 'data');
});Mocking Repositories
Data Repository
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'user_repository_test.mocks.dart';
class UserRepository {
final ApiClient apiClient;
UserRepository(this.apiClient);
Future<User> getUser(String id) async {
final data = await apiClient.get('/users/$id');
return User.fromJson(data);
}
}
@GenerateMocks([ApiClient])
void main() {
late MockApiClient mockApiClient;
late UserRepository userRepository;
setUp(() {
mockApiClient = MockApiClient();
userRepository = UserRepository(mockApiClient);
});
test('fetches and parses user', () async {
when(mockApiClient.get('/users/123'))
.thenAnswer((_) async => {
'id': '123',
'name': 'John Doe',
'email': 'john@example.com',
});
final user = await userRepository.getUser('123');
expect(user.id, '123');
expect(user.name, 'John Doe');
verify(mockApiClient.get('/users/123')).called(1);
});
}Mocking State Management
BLoC Mock
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterInitial()) {
on<IncrementEvent>((event, emit) => emit(CounterIncremented()));
}
}
// Test
test('emits states on increment', () {
final bloc = CounterBloc();
expectLater(bloc, emitsInOrder([
CounterInitial(),
CounterIncremented(),
]));
bloc.add(IncrementEvent());
});Provider Mock
class AuthProvider extends ChangeNotifier {
bool _isLoggedIn = false;
bool get isLoggedIn => _isLoggedIn;
void login() {
_isLoggedIn = true;
notifyListeners();
}
}
// Test
test('notifies listeners on login', () {
final provider = AuthProvider();
bool notified = false;
provider.addListener(() {
notified = true;
});
provider.login();
expect(notified, true);
expect(provider.isLoggedIn, true);
});Manual Mocks
Simple Manual Mock
class ApiService {
Future<String> fetchData() async => 'real data';
}
class MockApiService implements ApiService {
String? _mockedResponse;
void setMockResponse(String response) {
_mockedResponse = response;
}
@override
Future<String> fetchData() async => _mockedResponse ?? 'default mock';
}
// Test
test('uses manual mock', () async {
final mockService = MockApiService();
mockService.setMockResponse('test data');
final result = await mockService.fetchData();
expect(result, 'test data');
});State-Based Mock
class StatefulMockService implements ApiService {
final List<String> responses = [];
int _callCount = 0;
@override
Future<String> fetchData() async {
if (_callCount < responses.length) {
return responses[_callCount++];
}
throw Exception('No more responses');
}
void addResponse(String response) {
responses.add(response);
}
}
// Test
test('returns sequential responses', () async {
final mock = StatefulMockService();
mock.addResponse('first');
mock.addResponse('second');
expect(await mock.fetchData(), 'first');
expect(await mock.fetchData(), 'second');
await expectLater(mock.fetchData(), throwsException);
});Best Practices
1. Mock at boundaries - Mock external dependencies, not internal logic 2. Avoid over-mocking - Only mock what's necessary for the test 3. Verify interactions - Ensure mocks are called correctly 4. Use specific matchers - Avoid any when possible 5. Clear expectations - Make test intentions clear 6. Reset mocks - Use setUp/tearDown to clean up 7. Test real behavior - Verify expected outcomes, not implementation 8. Keep mocks simple - Complex mocks indicate design issues
Common Patterns
Mock Chain
when(mockService.first())
.thenReturn(mockSecondService);
when(mockSecondService.getData())
.thenReturn('result');
final result = mockService.first().getData();
expect(result, 'result');Mock Repository
class MockRepository {
final Map<String, dynamic> _data = {};
T get<T>(String key) => _data[key] as T;
void set<T>(String key, T value) => _data[key] = value;
}
// Test
test('uses mock repository', () {
final repo = MockRepository();
repo.set('name', 'John');
expect(repo.get<String>('name'), 'John');
});Mock Factory
class ServiceFactory {
static ApiService create() => RealApiService();
}
class MockServiceFactory {
ApiService? mockService;
ApiService create() => mockService ?? RealApiService();
}
// Test
test('uses factory mock', () {
final factory = MockServiceFactory();
factory.mockService = MockApiService();
when(factory.mockService!.getData()).thenReturn('mocked');
final service = factory.create();
expect(service.getData(), 'mocked');
});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.yamlDart 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.dartAndroid 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.testNativeMethodiOS 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");
}
@endRun iOS Tests
# From ios directory
cd example/ios
# Run tests
xcodebuild test -workspace Runner.xcworkspace -scheme Runner -configuration Debug
# Or run from Xcode
# Product > TestNative 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);
}
@endTesting 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 DebugUnit Testing Guide
Overview
Unit tests test individual functions, methods, or classes in isolation. They are the foundation of a well-tested Flutter app, providing fast feedback and high maintainability.
When to Write Unit Tests
- Testing business logic functions
- Validating data transformations
- Testing state management logic (Bloc, Provider, Riverpod)
- Mocking external services/API calls
- Testing utility functions and helpers
Test Structure
Basic Test
import 'package:test/test.dart';
void main() {
test('description', () {
// Arrange
final subject = MyClass();
// Act
final result = subject.myMethod();
// Assert
expect(result, expectedValue);
});
}Group Tests
import 'package:test/test.dart';
void main() {
group('Counter', () {
late Counter counter;
setUp(() {
counter = Counter();
});
test('value starts at 0', () {
expect(counter.value, 0);
});
test('increment increases value', () {
counter.increment();
expect(counter.value, 1);
});
tearDown(() {
counter.dispose();
});
});
}Testing Patterns
Testing Pure Functions
// Function to test
int add(int a, int b) => a + b;
// Test
test('add returns sum of two numbers', () {
expect(add(2, 3), 5);
expect(add(-1, 1), 0);
});Testing State Changes
class Counter {
int _value = 0;
int get value => _value;
void increment() => _value++;
void decrement() => _value--;
void reset() => _value = 0;
}
// Tests
group('Counter state changes', () {
late Counter counter;
setUp(() => counter = Counter());
test('initial value is 0', () {
expect(counter.value, 0);
});
test('increment increases value by 1', () {
counter.increment();
expect(counter.value, 1);
});
test('decrement decreases value by 1', () {
counter.decrement();
expect(counter.value, -1);
});
test('multiple increments work correctly', () {
counter.increment();
counter.increment();
counter.increment();
expect(counter.value, 3);
});
});Testing Async Operations
class DataService {
Future<String> fetchData() async {
await Future.delayed(const Duration(seconds: 1));
return 'data';
}
}
// Test
test('fetchData returns data after delay', () async {
final service = DataService();
final result = await service.fetchData();
expect(result, 'data');
});Testing Streams
class CounterStream {
final _controller = StreamController<int>();
Stream<int> get stream => _controller.stream;
void increment() => _controller.sink.add(1);
void dispose() => _controller.close();
}
// Test
test('stream emits values', () async {
final counter = CounterStream();
expectLater(
counter.stream,
emitsInOrder([1, 1, 1]),
);
counter.increment();
counter.increment();
counter.increment();
await Future.delayed(const Duration(milliseconds: 100));
counter.dispose();
});Matchers
Common Matchers
// Equality
expect(actual, equals(expected));
// Not equal
expect(actual, isNot(equals(expected)));
// Null checks
expect(value, isNull);
expect(value, isNotNull);
// Numeric comparisons
expect(5, greaterThan(3));
expect(5, lessThan(10));
expect(5, greaterThanOrEqualTo(5));
expect(5, lessThanOrEqualTo(5));
// Type checking
expect(obj, isA<String>());
expect(obj, isNot(isA<int>()));
// String matching
expect(text, contains('substring'));
expect(text, startsWith('prefix'));
expect(text, endsWith('suffix'));
expect(text, matches(RegExp(r'\d+')));
// Lists
expect(list, isEmpty);
expect(list, isNotEmpty);
expect(list, hasLength(3));
expect(list, contains(item));
expect(list, orderedEquals([1, 2, 3]));Custom Matchers
import 'package:test/test.dart';
class HasLength extends Matcher {
final int expectedLength;
HasLength(this.expectedLength);
@override
bool matches(item, Map matchState) {
return (item as List).length == expectedLength;
}
@override
Description describe(Description description) {
return description.add('has length $expectedLength');
}
}
// Usage
test('list has custom length', () {
final list = [1, 2, 3];
expect(list, HasLength(3));
});Exception Testing
class Calculator {
int divide(int a, int b) {
if (b == 0) throw ArgumentError('Cannot divide by zero');
return a ~/ b;
}
}
// Test
test('divide throws when dividing by zero', () {
final calculator = Calculator();
expect(
() => calculator.divide(10, 0),
throwsArgumentError,
);
});
test('divide throws with specific message', () {
final calculator = Calculator();
expect(
() => calculator.divide(10, 0),
throwsA(isA<ArgumentError>()
.having((e) => e.message, 'message', contains('zero'))),
);
});Mocking
Using Mockito
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'user_service_test.mocks.dart';
@GenerateMocks([ApiClient])
void main() {
group('UserService', () {
late MockApiClient mockApiClient;
late UserService userService;
setUp(() {
mockApiClient = MockApiClient();
userService = UserService(mockApiClient);
});
test('fetches user data', () async {
when(mockApiClient.getUser('123'))
.thenAnswer((_) async => User(id: '123', name: 'John'));
final user = await userService.getUser('123');
expect(user.name, 'John');
verify(mockApiClient.getUser('123')).called(1);
});
});
}Generate the mock file with:
dart run build_runner buildManual Mocking
class MockStorage implements Storage {
String? savedData;
@override
void save(String data) {
savedData = data;
}
@override
String? load() => savedData;
}
// Test
test('saves and loads data', () {
final mockStorage = MockStorage();
final service = DataService(mockStorage);
service.saveData('test');
expect(mockStorage.savedData, 'test');
final loaded = service.loadData();
expect(loaded, 'test');
});Testing with Fake Clock
import 'package:clock/clock.dart';
test('time-based operations', () {
withClock(Clock(() => DateTime(2024, 1, 1)), () {
final timestamp = DateTime.now();
expect(timestamp.year, 2024);
});
});Test Organization
File Structure
lib/
counter.dart
test/
counter/
counter_test.dart
counter_value_test.dart
counter_operations_test.dartNaming Conventions
- Test files:
*_test.dart - Test groups: Group related functionality
- Test names: Describe what and why
// Good
test('value increments when increment is called', () {});
// Avoid
test('increment', () {});Best Practices
1. Arrange-Act-Assert - Structure tests with clear sections 2. One assertion per test - When possible, keep tests focused 3. Descriptive names - Explain what and why, not just what 4. Avoid test interdependence - Each test should be independent 5. Use setUp/tearDown - Avoid code duplication 6. Test edge cases - Test boundaries, nulls, and errors 7. Keep tests fast - Avoid slow operations in unit tests 8. Mock external dependencies - Tests should be deterministic
Common Pitfalls
Testing Implementation Details
// Bad - tests implementation
test('counter calls _update', () {
verify(counter._update()).called(1);
});
// Good - tests behavior
test('counter value increases after increment', () {
expect(counter.value, 1);
});Fragile Tests
// Bad - depends on exact timing
test('completes within 100ms', () async {
final stopwatch = Stopwatch()..start();
await operation();
expect(stopwatch.elapsedMilliseconds, lessThan(100));
});
// Good - tests completion
test('completes successfully', () async {
await operation();
// operation completed without error
});Running Tests
# Run all tests
flutter test
# Run specific file
flutter test test/counter/counter_test.dart
# Run with coverage
flutter test --coverage
# Run verbose
flutter test --verbose
# Run specific test
flutter test --name "value increments"Widget Testing Guide
Overview
Widget tests (component tests) verify that widgets render correctly and interact as expected. They run in a test environment that simulates widget lifecycle without a full UI system.
When to Write Widget Tests
- Verifying widget rendering
- Testing user interactions (taps, drags, scrolling)
- Validating widget state changes
- Testing different orientations and screen sizes
- Testing form inputs and validation
Test Structure
Basic Widget Test
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('widget description', (tester) async {
// Build widget
await tester.pumpWidget(const MyWidget());
// Find and verify
expect(find.text('Hello'), findsOneWidget);
});
}Using MaterialApp
testWidgets('widget with MaterialApp', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: MyWidget(),
),
),
);
expect(find.byType(MyWidget), findsOneWidget);
});Finding Widgets
Common Finders
// By text
find.text('Hello')
find.textContaining('ello')
find.textRegExp(RegExp(r'\d+'))
// By widget type
find.byType(Text)
find.byType(ElevatedButton)
find.byType(MyWidget)
// By key
find.byKey(const Key('my-button'))
find.byKey(const ValueKey('counter'))
// By icon
find.byIcon(Icons.add)
// By widget instance
find.byWidget(myWidgetInstance)
// By specific widget ancestor
find.ancestor(
of: find.text('Child'),
matching: find.byType(Column),
)Finder Matchers
// Find exactly one
expect(find.text('Hello'), findsOneWidget);
// Find multiple
expect(find.text('Hello'), findsWidgets);
// Find nothing
expect(find.text('Missing'), findsNothing);
// Find at least N
expect(find.text('Item'), findsNWidgets(3));User Interactions
Tapping
// Tap a widget
await tester.tap(find.byType(ElevatedButton));
// Trigger a frame
await tester.pump();
// Wait for all animations
await tester.pumpAndSettle();Dragging
// Drag a widget
await tester.drag(
find.byType(MyWidget),
const Offset(0, -300),
);
await tester.pumpAndSettle();Entering Text
// Enter text into TextField
await tester.enterText(
find.byType(TextField),
'Hello World',
);
// Tap to focus, then enter
await tester.tap(find.byType(TextField));
await tester.enterText(find.byType(TextField), 'Text');
// Enter by key
await tester.enterText(
find.byKey(const Key('email-field')),
'test@example.com',
);Scrolling
// Scroll a ListView
await tester.fling(
find.byType(ListView),
const Offset(0, -500),
10000,
);
await tester.pumpAndSettle();
// Scroll until widget is visible
await tester.scrollUntilVisible(
find.text('Target Item'),
500.0,
);Long Press
await tester.longPress(find.byType(IconButton));
await tester.pumpAndSettle();Testing Widget Properties
Text Content
testWidgets('displays correct text', (tester) async {
await tester.pumpWidget(const MyWidget(text: 'Hello'));
expect(find.text('Hello'), findsOneWidget);
expect(find.text('Goodbye'), findsNothing);
});Widget State
testWidgets('updates state on button press', (tester) async {
await tester.pumpWidget(const MyApp());
// Initial state
expect(find.text('Count: 0'), findsOneWidget);
// Tap increment button
await tester.tap(find.byKey(const Key('increment')));
await tester.pumpAndSettle();
// Updated state
expect(find.text('Count: 1'), findsOneWidget);
});Conditional Rendering
testWidgets('shows loading indicator when loading', (tester) async {
await tester.pumpWidget(MyWidget(isLoading: true));
expect(find.byType(CircularProgressIndicator), findsOneWidget);
expect(find.byType(ElevatedButton), findsNothing);
});
testWidgets('shows button when not loading', (tester) async {
await tester.pumpWidget(MyWidget(isLoading: false));
expect(find.byType(CircularProgressIndicator), findsNothing);
expect(find.byType(ElevatedButton), findsOneWidget);
});Testing with Arguments
testWidgets('receives and displays title', (tester) async {
const title = 'My Title';
await tester.pumpWidget(
MaterialApp(
home: MyWidget(title: title),
),
);
expect(find.text(title), findsOneWidget);
});
testWidgets('receives and uses callback', (tester) async {
bool callbackCalled = false;
await tester.pumpWidget(
MaterialApp(
home: MyWidget(
onPressed: () => callbackCalled = true,
),
),
);
await tester.tap(find.byType(ElevatedButton));
expect(callbackCalled, true);
});Testing Orientation
testWidgets('widget in portrait mode', (tester) async {
await tester.binding.setSurfaceSize(const Size(400, 800));
await tester.pumpWidget(const MyApp());
// Verify portrait layout
expect(find.byType(Column), findsOneWidget);
// Clean up
addTearDown(tester.binding.setSurfaceSize(null));
});
testWidgets('widget in landscape mode', (tester) async {
await tester.binding.setSurfaceSize(const Size(800, 400));
await tester.pumpWidget(const MyApp());
// Verify landscape layout
expect(find.byType(Row), findsOneWidget);
// Clean up
addTearDown(tester.binding.setSurfaceSize(null));
});Testing Scrollable Widgets
ListView Scrolling
testWidgets('scrolling in ListView', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) => ListTile(
title: Text('Item $index'),
),
),
),
);
// Initial item visible
expect(find.text('Item 0'), findsOneWidget);
expect(find.text('Item 50'), findsNothing);
// Scroll down
await tester.fling(
find.byType(ListView),
const Offset(0, -5000),
10000,
);
await tester.pumpAndSettle();
// Later item now visible
expect(find.text('Item 50'), findsOneWidget);
});Scroll Until Visible
testWidgets('scroll until specific item is visible', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) => ListTile(
key: ValueKey('item-$index'),
title: Text('Item $index'),
),
),
),
);
// Scroll until item 75 is visible
await tester.scrollUntilVisible(
find.byKey(const ValueKey('item-75')),
500.0,
);
expect(find.text('Item 75'), findsOneWidget);
});Testing Forms
TextField Validation
testWidgets('validates email field', (tester) async {
final formKey = GlobalKey<FormState>();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Form(
key: formKey,
child: Column(
children: [
TextFormField(
key: const Key('email'),
validator: (value) {
if (value == null || !value.contains('@')) {
return 'Invalid email';
}
return null;
},
),
ElevatedButton(
key: const Key('submit'),
onPressed: () => formKey.currentState!.validate(),
child: const Text('Submit'),
),
],
),
),
),
),
);
// Enter invalid email
await tester.enterText(find.byKey(const Key('email')), 'invalid');
await tester.tap(find.byKey(const Key('submit')));
await tester.pump();
// Should show error
expect(find.text('Invalid email'), findsOneWidget);
// Enter valid email
await tester.enterText(find.byKey(const Key('email')), 'test@example.com');
await tester.tap(find.byKey(const Key('submit')));
await tester.pump();
// Should hide error
expect(find.text('Invalid email'), findsNothing);
});Multiple Form Fields
testWidgets('form with multiple fields', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Form(
child: Column(
children: [
TextFormField(
key: const Key('name'),
decoration: const InputDecoration(labelText: 'Name'),
),
TextFormField(
key: const Key('email'),
decoration: const InputDecoration(labelText: 'Email'),
),
ElevatedButton(
key: const Key('submit'),
onPressed: () {},
child: const Text('Submit'),
),
],
),
),
),
),
);
// Fill form
await tester.enterText(find.byKey(const Key('name')), 'John Doe');
await tester.enterText(find.byKey(const Key('email')), 'john@example.com');
// Submit
await tester.tap(find.byKey(const Key('submit')));
await tester.pumpAndSettle();
});Testing Animations
testWidgets('animation plays correctly', (tester) async {
await tester.pumpWidget(const AnimatedWidget());
// Initial state
expect(find.byType(Opacity), findsOneWidget);
// Trigger animation
await tester.tap(find.byType(ElevatedButton));
// Pump animation frames
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
await tester.pump(const Duration(milliseconds: 100));
// Animation complete
await tester.pumpAndSettle();
// Verify final state
final opacity = tester.widget<Opacity>(find.byType(Opacity));
expect(opacity.opacity, 0.0);
});Testing Custom Widgets
Testing InheritedWidgets
class MyInheritedWidget extends InheritedWidget {
final String data;
const MyInheritedWidget({
super.key,
required this.data,
required Widget child,
}) : super(child: child);
static MyInheritedWidget of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>()!;
}
@override
bool updateShouldNotify(MyInheritedWidget oldWidget) =>
data != oldWidget.data;
}
testWidgets('inherited widget provides data', (tester) async {
await tester.pumpWidget(
MyInheritedWidget(
data: 'Test Data',
child: Builder(
builder: (context) {
return Text(MyInheritedWidget.of(context).data);
},
),
),
);
expect(find.text('Test Data'), findsOneWidget);
});Testing StatefulWidget
class CounterWidget extends StatefulWidget {
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _count = 0;
void _increment() => setState(() => _count++);
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $_count'),
ElevatedButton(
key: const Key('increment'),
onPressed: _increment,
child: const Text('Increment'),
),
],
);
}
}
testWidgets('CounterWidget increments correctly', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CounterWidget(),
),
),
);
// Initial count
expect(find.text('Count: 0'), findsOneWidget);
// Increment
await tester.tap(find.byKey(const Key('increment')));
await tester.pump();
expect(find.text('Count: 1'), findsOneWidget);
});Testing with Mock Data
class ProductList extends StatelessWidget {
final List<Product> products;
const ProductList({super.key, required this.products});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) => ListTile(
title: Text(products[index].name),
subtitle: Text('\$${products[index].price}'),
),
);
}
}
testWidgets('displays list of products', (tester) async {
final products = [
Product(name: 'Product 1', price: 10.99),
Product(name: 'Product 2', price: 20.99),
];
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ProductList(products: products),
),
),
);
expect(find.text('Product 1'), findsOneWidget);
expect(find.text('\$10.99'), findsOneWidget);
expect(find.text('Product 2'), findsOneWidget);
expect(find.text('\$20.99'), findsOneWidget);
});Testing Accessibility
testWidgets('widget has proper semantics', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Semantics(
label: 'Submit button',
button: true,
child: ElevatedButton(
onPressed: () {},
child: const Text('Submit'),
),
),
),
);
final semantics = tester.getSemantics(find.byType(ElevatedButton));
expect(semantics.label, 'Submit button');
expect(semantics.hasAction(SemanticsAction.tap), true);
});Best Practices
1. Use descriptive test names - Explain what and why 2. Keep tests independent - Each test should be standalone 3. Use pumpAndSettle() - Wait for all animations 4. Test user behavior, not implementation - Focus on what users see and do 5. Group related tests - Use group() for organization 6. Test edge cases - Empty lists, null values, errors 7. Use keys for widgets - Makes finding easier and more reliable 8. Test accessibility - Verify semantic labels and actions
Common Patterns
Testing Navigation
testWidgets('navigates to detail screen', (tester) async {
await tester.pumpWidget(
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => const HomeScreen(),
'/detail': (context) => const DetailScreen(),
},
),
);
// Tap navigation button
await tester.tap(find.text('Go to Detail'));
await tester.pumpAndSettle();
// Verify navigation
expect(find.text('Detail Screen'), findsOneWidget);
expect(find.text('Home Screen'), findsNothing);
});Testing Loading States
testWidgets('shows loading then content', (tester) async {
bool isLoading = true;
await tester.pumpWidget(
MaterialApp(
home: StatefulBuilder(
builder: (context, setState) {
return isLoading
? const CircularProgressIndicator()
: const Text('Content Loaded');
},
),
),
);
expect(find.byType(CircularProgressIndicator), findsOneWidget);
// Simulate data load
isLoading = false;
await tester.pump();
expect(find.byType(CircularProgressIndicator), findsNothing);
expect(find.text('Content Loaded'), findsOneWidget);
});#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SKILL_FILE="$ROOT_DIR/SKILL.md"
fail() {
echo "ERROR: $*" >&2
exit 1
}
[[ -f "$SKILL_FILE" ]] || fail "Missing SKILL.md"
ruby -ryaml -e '
path = ARGV.fetch(0)
text = File.read(path)
unless text.start_with?("---\n")
abort "ERROR: SKILL.md must start with YAML frontmatter"
end
parts = text.split(/^---\s*$/, 3)
abort "ERROR: SKILL.md frontmatter is not closed" unless parts.length == 3
frontmatter = YAML.safe_load(parts[1], permitted_classes: [], aliases: false)
abort "ERROR: frontmatter must be a map" unless frontmatter.is_a?(Hash)
name = frontmatter["name"]
description = frontmatter["description"]
abort "ERROR: missing frontmatter name" if name.to_s.empty?
abort "ERROR: missing frontmatter description" if description.to_s.empty?
expected = File.basename(File.dirname(path))
abort "ERROR: frontmatter name #{name.inspect} does not match #{expected.inspect}" unless name == expected
required_terms = %w[unit widget integration mock plugin]
missing = required_terms.reject { |term| description.downcase.include?(term) }
abort "ERROR: description missing trigger terms: #{missing.join(", ")}" unless missing.empty?
' "$SKILL_FILE"
ruby -e '
root = ARGV.fetch(0)
failed = false
Dir[File.join(root, "**/*.md")].sort.each do |file|
File.readlines(file).each_with_index do |line, index|
line.scan(/\[[^\]]+\]\(([^)]+)\)/).flatten.each do |target|
next if target.match?(/\A(?:https?:|mailto:|#)/)
path = target.split("#", 2).first
next if path.empty?
full_path = File.expand_path(path, File.dirname(file))
unless File.exist?(full_path)
warn "ERROR: missing local link #{file}:#{index + 1} -> #{target}"
failed = true
end
end
end
end
exit(failed ? 1 : 0)
' "$ROOT_DIR"
while IFS= read -r -d '' file; do
count="$(grep -c '^```' "$file" || true)"
if (( count % 2 != 0 )); then
fail "Unbalanced fenced code blocks in $file"
fi
done < <(find "$ROOT_DIR" -name '*.md' -print0)
[[ -z "$(find "$ROOT_DIR" -name '.DS_Store' -print)" ]] || fail "Remove .DS_Store files from flutter-testing"
grep -q 'scripts/verify-examples.sh' "$SKILL_FILE" || fail "SKILL.md must route to scripts/verify-examples.sh"
for pattern in \
'tester\.trace\(' \
'tester\.takeScreenshot\(' \
'flutter test --platform' \
'--no-sound-null-safety' \
'captureNamed' \
'flutter pub run build_runner build' \
'setSurfaceSize\(Size\.zero\)'
do
if rg -n --glob '*.md' -- "$pattern" "$ROOT_DIR/references"; then
fail "Forbidden stale pattern found: $pattern"
fi
done
echo "flutter-testing skill checks passed"
Related skills
How it compares
Use flutter-testing for fast diagnosis of known Flutter test error patterns; consult Flutter layout docs when designing new responsive layouts from scratch.
FAQ
What does flutter-testing do?
Apply flutter-testing agent skill workflows from documented SKILL.md guidance.
When should I use flutter-testing?
During ship testing work for testing & qa.
Is flutter-testing safe to install?
Review the Security Audits panel on this listing before production use.