SuperAPI AI Directory

testing

Testing & quality

View repository

Updated 10-16 01:12

About this skill

Dart & 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 group for the class, nested group for the method, test for 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/tearDown calls live inside a group, never at the top level of main()
  • Initialize mutable objects in setUp() with late — declare late MyDep dep; then assign in setUp so 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 — never package:mockito
  • Constant test tags — use an abstract class TestTag with static const fields; 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 pumpApp test helper — wrap widgets via shared helper in test/helpers/pump_app.dart; never inline pumpWidget(MaterialApp(...))
  • Tag all golden tests — annotate with TestTag.golden so goldens can run/update independently
  • Pass directory to the test MCP tool when the project is not at the workspace root — monorepos with the Flutter project in a subdirectory (e.g. mobile/) require directory: 'mobile'; omit it only when pubspec.yaml is at the workspace root
  • Pass timeout_seconds to the test MCP tool — Flutter tests can hang indefinitely when pumpAndSettle() 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 test MCP tool — on Claude Code use mcp__very-good-cli__test; on a host without this plugin's Bash hooks and without that MCP server connected, run very_good test (or flutter 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: 'Dash@Example.COM');

  final captured = verify(() => repository.save(captureAny())).captured;
  expect(captured.first.email, equals('dash@example.com'));
});

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