About testing
A testing skill for Dart and Flutter developers that applies VGV conventions to unit and widget tests. It covers readable structure, isolation, mocktail, widget helpers, golden tests, and guidance for running tests.
Key features
- 01Descriptive hierarchical test naming
- 02Isolated setup and private mocks
- 03mocktail-based mocking conventions
- 04Widget tests with pumpApp
- 05Golden test tagging and visual checks
- 06Test MCP tool execution guidance
Use cases
- 01Organizing Dart unit tests
- 02Writing behavior-focused widget tests
- 03Reducing shared state between tests
- 04Running tests in Flutter monorepos
MCP support
The skill includes guidance for running tests with a test MCP tool, including directory and timeout options. It names mcp__very-good-cli__test as a Claude Code option and describes command-line fallbacks when that server is unavailable.
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ verygoodopensource/ vgv-ai-flutter-plugin --skill testingAfter installing, type/testingin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
agents/openai.yamlreferences/animation-testing.mdconfiguration.mdcoverage.mdgolden-tests.mdmatchers.mdmocktail.mdtest-patterns.mdwidget-tests.mdDetails
From the project READMEDart & Flutter Testing
VGV testing conventions for Dart and Flutter. The framework APIs are assumed; what follows is the house style layered on top of them.
Core Standards
Apply these standards to all test work:
- Descriptive test names — verbose, readable names that describe the behavior; never
'works'or'renders' - Hierarchical group/test structure that reads as natural sentences — top-level
groupfor the class, nestedgroupfor the method,testfor the behavior (e.g.,UserRepository→getUser→returns User when API succeeds) - String interpolation for type references — use
'returns $User'not'returns User'so renames propagate automatically - Private mocks per file — declare
class _MockX extends Mock implements X {}with underscore prefix to prevent cross-file coupling - Contained test setup within groups — all
setUp/tearDowncalls live inside agroup, never at the top level ofmain() - Initialize mutable objects in
setUp()withlate— declarelate MyDep dep;then assign insetUpso each test gets a fresh instance - No shared mutable state between tests — never use static members, global variables, or top-level final instances that persist across tests
- Use
package:mocktail— neverpackage:mockito - Constant test tags — use an
abstract class TestTagwithstatic constfields; never pass raw string literals as tags - Test behavior, not properties — widget tests focus on functional outcomes; static visual properties validated via golden tests
- Use
pumpApptest helper — wrap widgets via shared helper intest/helpers/pump_app.dart; never inlinepumpWidget(MaterialApp(...)) - Tag all golden tests — annotate with
TestTag.goldenso goldens can run/update independently - Pass
directoryto thetestMCP tool when the project is not at the workspace root — monorepos with the Flutter project in a subdirectory (e.g.mobile/) requiredirectory: 'mobile'; omit it only whenpubspec.yamlis at the workspace root - Pass
timeout_secondsto thetestMCP tool — Flutter tests can hang indefinitely whenpumpAndSettle()is called without a timeout; set a cap (e.g.timeout_seconds: 120) so the run is killed instead of stalling - Cross-harness fallback for the
testMCP tool — on Claude Code usemcp__very-good-cli__test; on a host without this plugin's Bash hooks and without that MCP server connected, runvery_good test(orflutter test/dart test) directly with the same coverage and timeout options — never block on a missing MCP server
File Organization
| Convention | Rule |
|---|---|
| File suffix | Every test file ends with _test.dart |
| Directory | All tests live under test/ |
| Mirror structure | test/ mirrors lib/ exactly — lib/src/models/user.dart → test/src/models/user_test.dart |
| Helpers | Shared test utilities go in test/helpers/ (e.g., pump_app.dart, fakes.dart) |
Group and Test Hierarchy
Structure groups so that concatenated descriptions read as natural sentences. Use the
PascalCase type itself — not a string — in the top-level group.
class _MockApiClient extends Mock implements ApiClient {}
void main() {
group(UserRepository, () {
late ApiClient apiClient;
late UserRepository subject;
setUp(() {
apiClient = _MockApiClient();
subject = UserRepository(apiClient: apiClient);
});
group('getUser', () {
test('returns $User when API call succeeds', () async {
when(() => apiClient.fetchUser(any()))
.thenAnswer((_) async => User(id: '1', name: 'Dash'));
final result = await subject.getUser('1');
expect(result, equals(User(id: '1', name: 'Dash')));
verify(() => apiClient.fetchUser('1')).called(1);
});
test('throws $UserNotFoundException when API returns 404', () {
when(() => apiClient.fetchUser(any()))
.thenThrow(ApiException(statusCode: 404));
expect(
() => subject.getUser('1'),
throwsA(isA<UserNotFoundException>()),
);
});
});
});
}
Naming Conventions
| Pattern | Example |
|---|---|
| Returns a value | 'returns $User when API call succeeds' |
| Throws an exception | 'throws $UserNotFoundException when user is not found' |
| Calls a dependency | 'calls apiClient.deleteUser with correct id' |
| Emits states | 'emits [loading, success] when data is fetched' |
| Conditional behavior | 'returns cached value when cache is not expired' |
| Edge case | 'returns empty list when repository has no items' |
Test Isolation
Each test must pass when run individually, in any order, and in parallel. Use
--test-randomize-ordering-seed random to expose hidden dependencies.
| Anti-Pattern | Problem | Correct Approach |
|---|---|---|
setUp at the top level of main() | Breaks when test runner merges files for optimization | Move setUp inside a group |
final dep = _MockDep(); (top-level) | Same instance shared across all tests; state leaks | Use late + setUp inside a group |
class MockDep extends Mock (public) | Other test files can import and depend on it | Use class _MockDep extends Mock (private) |
| Static/global mutable variables | State persists across tests | Reset in setUp or avoid entirely |
| Tests that must run in a specific order | Fragile, fails with random ordering | Make each test fully self-contained |
Testing Private Logic
Never test private methods directly. Exercise private logic through the public method that uses it:
// If _normalizeEmail is private, test it through the public createUser method:
test('normalizes email to lowercase before saving', () async {
when(() => repository.save(any())).thenAnswer((_) async {});
await subject.createUser(email: '[email protected]');
final captured = verify(() => repository.save(captureAny())).captured;
expect(captured.first.email, equals('[email protected]'));
});
Widget Testing
| Rule | Details |
|---|---|
Use testWidgets | Every widget test uses testWidgets instead of test |
Prefer find.byType | Default finder; use find.text for user-visible content, find.byKey only when type/text is ambiguous |
| Group by behavior category | Use renders, navigates, calls [MethodName], updates as nested group names |
| Focus on behavior | Assert what the widget does (shows text, calls callback, navigates); use golden tests for visual appearance |
| Mock Blocs and Cubits | Use MockBloc/MockCubit from package:bloc_test; never provide real Blocs in widget tests |
Prefer pump() over pumpAndSettle() | pumpAndSettle hangs on infinite animations such as CircularProgressIndicator |
pumpApp Helper
Every widget test wraps the widget under test through one shared helper, so no test
inlines its own MaterialApp:
// test/helpers/pump_app.dart
extension PumpApp on WidgetTester {
Future<void> pumpApp(Widget widget) {
return pumpWidget(
MaterialApp(
home: widget,
),
);
}
}
Export it from test/helpers/helpers.dart so every test imports it with one line:
import '../helpers/helpers.dart';
void main() {
group(MyWidget, () {
testWidgets('renders greeting text', (tester) async {
await tester.pumpApp(const MyWidget());
expect(find.text('Hello'), findsOneWidget);
});
});
}
Widget Testing Anti-Patterns
| Anti-Pattern | Problem | Correct Approach |
|---|---|---|
Inline MaterialApp in each test | Duplicated boilerplate; inconsistent setup | Use pumpApp helper |
find.byKey as default finder | Couples tests to implementation keys | Prefer find.byType or find.text |
| Testing padding, colors, or font sizes | Fragile; breaks on every design tweak; asserts appearance, not behavior | Use a golden test for visual validation |
Missing pump() after interaction | Widget tree does not rebuild; assertion sees stale state | Always pump() after tap, enterText, etc. |
| Real Blocs in widget tests | Tests become integration tests; slow, brittle, hard to isolate | Use MockBloc/MockCubit from bloc_test |
Padding, background color, and font size are the recurring temptation. Asserting them in a
widget test couples the test to the design system and breaks on every restyle — move them to
a golden test tagged TestTag.golden.
Additional Resources
- references/mocktail.md — mocks and fakes, stubbing, argument matchers, verification, fallback values
- references/test-patterns.md —
setUp/tearDownlifecycle and patterns for async, streams, exceptions,Equatable, and callbacks - references/widget-tests.md — full Bloc-backed widget test, themes/localization in
pumpApp, pumping methods, finders, and interactions - references/golden-tests.md — golden file testing (setup, writing goldens, tagging, running/updating, anti-patterns)
- references/matchers.md — matchers quick reference
- references/configuration.md —
dart_test.yamlconfiguration (tags, platform overrides) and running tests via the MCPtesttool - references/coverage.md — coverage patterns and package/imports reference
- references/animation-testing.md — testing implicit/explicit animations, AnimatedSwitcher, page transitions, and injected controllers