agentleFS
alfredobs97alfredobs97/flutter_agentic_template/.claude/skills/testing/SKILL.md

testing

Skill0 starsChanged 49 days agoMITView on GitHub

Covers the testing pattern for every layer — domain (package:test with a hand-written Fake data provider, no mocktail), data (an ephemeral real instance or a mocked client), ui Cubit/BLoC (blocTest + mocktail against the domain repository), ui widgets (the pumpApp helper), and router tests (a fresh buildAppRouter() per test); triggers whenever a test is being written for a new entity, provider, repository, Cubit/BLoC, widget, or route.

194 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use bundle-dependency-prs, update-library-template and 10 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • MicrosoftSkill

    bundle-dependency-prs

    Fix broken dependency update PRs and aggregate the ones that work into one PR.

    Also in 7 projects that use this

  • MicrosoftSkill

    update-library-template

    Merges the latest Library.Template into this repo (at position of HEAD) and resolves conflicts.

    Also in 7 projects that use this

  • zetaloopSkill

    assign-copilot

    Assigns a GitHub issue to the Copilot coding agent, optionally specifying a custom agent. Use this when asked to assign an issue to Copilot or delegate an issue to CCA.

    Also in 3 projects that use this

  • NVIDIA-NeMoSkill

    build-and-dependency

    Dev environment setup for Megatron Bridge — container-based development, uv package management, lockfile regeneration, adding dependencies, Slurm container usage, and common build pitfalls.

    Also in 3 projects that use this

  • timmo001Skill

    changeset-scope

    Keep all scoped code work contained to the user-defined changeset. Use for implementation, fixes, diagnosis, refactoring, cleanup, and review when explicit instructions, named files, diffs, branches, pull requests, or injected work scopes define the boundary.

    Also in 3 projects that use this

  • JetBrainsSkill

    module-dependencies

    Add or modify IntelliJ module dependencies in `.iml` files.

    Also in 3 projects that use this

The file

We split this skill into the rules it gives an agent

5 rules. 0 are common, 0 are distinctive, and 5 are very specific to this project.

  • 0

    Common

    Most of the 174 edited versions we read keep these.

  • 0

    Distinctive

    This author's own choices. Worth a look.

  • 5

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Put a reusable Fake*DataProvider in packages/domain/test/helpers/ if more than one test file needs it, rather than redeclaring it per file.

    line 72
  2. Very specific:

    Only fall back to a mocked client (mocktail) when the chosen stack has no ephemeral/in-memory mode to test against (e.g.

    line 124
  3. Very specific:

    If a new feature needs additional wiring beyond what pumpApp provides (e.g.

    line 263
  4. Very specific:

    Every subsequent test for a screen that needs a TaskRepository reuses pumpAppWithTasks — it is written once, in test/helpers/, and never …

    line 298
  5. Very specific:

    Never declare a Mock/Fake class inside lib/ in any package (root AGENTS.md sections 12 and 20) — it belongs in test/ as a …

    line 353

“Common” is measured: kept by at least 6 in 10 of the 174 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Testing per layer

Root AGENTS.md section 6: every new entity, data provider, repository, Cubit/BLoC, widget, or route gets a test in the same change that introduces it. A change is not done until fvm dart run tool/quality_gate.dart passes. Root AGENTS.md section 12: mocking packages and hand-written fakes live under test/ only, as dev_dependencies — never inside lib/.

The rule that matters most across every layer below: assert the actual interesting behavior — the state that gets emitted, the failure that gets mapped, the widget that gets shown — not just "it runs without throwing". A test that only calls a method and asserts nothing meaningful about the result is not a passing bar for "has a test".

1. Domain layer: package:test, hand-written Fake*, no mocking package

packages/domain has no Flutter dependency and no mocktail dependency (packages/domain/pubspec.yaml) — it uses plain package:test. A repository test uses a hand-written fake implementing the data-provider interface, not a mock:

// packages/domain/test/repositories/task_repository_test.dart
import 'package:domain/domain.dart';
import 'package:test/test.dart';

class FakeTaskDataProvider implements TaskDataProvider {
  final List<Task> _tasks = [];

  @override
  Future<List<Task>> getAll() async => List.unmodifiable(_tasks);

  @override
  Future<void> add(Task task) async => _tasks.add(task);
}

void main() {
  group('TaskRepository', () {
    test('getAll returns tasks added through the data provider', () async {
      final provider = FakeTaskDataProvider();
      final repository = TaskRepository(provider);
      final task = Task(id: '1', title: 'Buy milk');

      await repository.add(task);
      final result = await repository.getAll();

      expect(result, [task]);
    });
  });
}

This mirrors the shipped example, packages/domain/test/repositories/feature_flag_repository_test.dart, which constructs a FeatureFlagRepository directly (it has no data-provider to fake) and asserts the actual delegated value:

test('delegates isEnabled to the underlying FeatureFlags', () {
  const repository = FeatureFlagRepository(FeatureFlags({FeatureFlag.exampleFeature: true}));

  expect(repository.isEnabled(FeatureFlag.exampleFeature), isTrue);
});

Put a reusable Fake*DataProvider in packages/domain/test/helpers/ if more than one test file needs it, rather than redeclaring it per file.

2. Data layer: an ephemeral real instance, or a mocked client

packages/data uses flutter_test (kept for parity, and because most storage stacks pull in Flutter-plugin dependencies). Prefer a real, ephemeral instance of whatever stack choose-data-stack set up (an in-memory Drift database, a temp-directory Hive box) over mocking it — this exercises real query/serialization behavior instead of a hand-tuned stub:

// packages/data/test/data_providers/drift_task_data_provider_test.dart
import 'package:data/data.dart';
import 'package:domain/domain.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  group('DriftTaskDataProvider', () {
    late AppDatabase database;
    late DriftTaskDataProvider provider;

    setUp(() {
      database = AppDatabase.forTesting(NativeDatabase.memory());
      provider = DriftTaskDataProvider(database);
    });

    tearDown(() => database.close());

    test('add then getAll round-trips a task', () async {
      await provider.add(const Task(id: '1', title: 'Buy milk'));

      final tasks = await provider.getAll();

      expect(tasks, [const Task(id: '1', title: 'Buy milk')]);
    });

    test('wraps a constraint violation as a ValidationFailure', () async {
      await provider.add(const Task(id: '1', title: 'Buy milk'));

      // Duplicate primary key — the real database rejects it, and the
      // provider's guard() must translate that into a DomainFailure.
      await expectLater(
        provider.add(const Task(id: '1', title: 'Duplicate')),
        throwsA(isA<ValidationFailure>()),
      );
    });
  });
}

Only fall back to a mocked client (mocktail) when the chosen stack has no ephemeral/in-memory mode to test against (e.g. a third-party REST API with no local fake server) — then mock at the client boundary (Dio, http.Client), not the data provider itself:

class MockDioClient extends Mock implements Dio {}

test('maps a 404 response to NotFoundFailure', () async {
  final client = MockDioClient();
  when(() => client.get(any())).thenThrow(
    DioException(requestOptions: RequestOptions(), response: Response(
      requestOptions: RequestOptions(),
      statusCode: 404,
    )),
  );
  final provider = RestTaskDataProvider(client);

  await expectLater(provider.getAll(), throwsA(isA<NotFoundFailure>()));
});

3. UI Cubit/BLoC: blocTest + mocktail on the domain repository

A Cubit/Bloc test mocks the domain repository it depends on with mocktail — never the data provider underneath it (that's a different layer's test, per packages/ui/AGENTS.md):

// packages/ui/test/features/tasks/bloc/task_list_cubit_test.dart
import 'package:bloc_test/bloc_test.dart';
import 'package:domain/domain.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:ui/ui.dart';

class MockTaskRepository extends Mock implements TaskRepository {}

void main() {
  late MockTaskRepository repository;

  setUp(() => repository = MockTaskRepository());

  group('TaskListCubit', () {
    final tasks = [const Task(id: '1', title: 'Buy milk')];

    blocTest<TaskListCubit, TaskListState>(
      'load emits loading then loaded with the repository result',
      setUp: () => when(() => repository.getAll()).thenAnswer((_) async => tasks),
      build: () => TaskListCubit(repository),
      act: (cubit) => cubit.load(),
      expect: () => [
        const TaskListState(status: TaskListStatus.loading),
        TaskListState(status: TaskListStatus.loaded, tasks: tasks),
      ],
      verify: (_) => verify(() => repository.getAll()).called(1),
    );

    blocTest<TaskListCubit, TaskListState>(
      'load emits an error state when the repository throws a DomainFailure',
      setUp: () => when(() => repository.getAll()).thenThrow(const ConnectivityFailure('offline')),
      build: () => TaskListCubit(repository),
      act: (cubit) => cubit.load(),
      expect: () => [
        const TaskListState(status: TaskListStatus.loading),
        const TaskListState(status: TaskListStatus.error, error: TaskListError.connectivity),
      ],
    );
  });
}

Both tests assert the actual emitted TaskListState values (including which TaskListError the failure mapped to) — not just that load() completed without throwing.

registerFallbackValue for complex argument types

mocktail's any() needs a registered fallback for any non-primitive argument type used with when/verify, registered once per test run (typically in a setUpAll):

class FakeTask extends Fake implements Task {}

void main() {
  setUpAll(() => registerFallbackValue(FakeTask()));

  // Now `when(() => repository.add(any())).thenAnswer(...)` works for a
  // Task argument.
}

Without it, any()/captureAny() on a Task (or any other non-builtin type) throws at test setup time, not silently — but registering it up front avoids the noise entirely.

4. UI widgets: pumpApp, not per-file provider wiring

packages/ui/test/helpers/pump_app.dart wires every app-wide provider a screen expects (FeatureFlagRepository + FeatureFlagCubit, theme, localization delegates) so an individual widget test doesn't redeclare that boilerplate:

extension PumpApp on WidgetTester {
  Future<void> pumpApp(Widget widget, {FeatureFlagRepository? featureFlagRepository}) {
    return pumpWidget(
      RepositoryProvider<FeatureFlagRepository>.value(
        value: featureFlagRepository ?? const FeatureFlagRepository(FeatureFlags({})),
        child: BlocProvider(
          create: (context) => FeatureFlagCubit(context.read<FeatureFlagRepository>()),
          child: MaterialApp(
            theme: AppTheme.light,
            darkTheme: AppTheme.dark,
            localizationsDelegates: AppLocalizations.localizationsDelegates,
            supportedLocales: AppLocalizations.supportedLocales,
            home: widget,
          ),
        ),
      ),
    );
  }
}

A widget test just calls tester.pumpApp(const TaskListScreen()) and asserts on the result — mirroring packages/ui/test/features/home/home_screen_test.dart:

testWidgets('renders the localized title and placeholder copy', (tester) async {
  await tester.pumpApp(const HomeScreen());

  expect(find.text('Home'), findsOneWidget);
  expect(find.text('Run the new-feature skill to replace this screen.'), findsOneWidget);
});

If a new feature needs additional wiring beyond what pumpApp provides (e.g. a TaskRepository for a screen that creates its own Cubit), add a second pumper to pump_app.dart rather than rebuilding the provider tree inline in the new test file:

// packages/ui/test/helpers/pump_app.dart — add alongside pumpApp
extension PumpApp on WidgetTester {
  // ... existing pumpApp ...

  Future<void> pumpAppWithTasks(Widget widget, {required TaskRepository taskRepository}) {
    return pumpWidget(
      MultiRepositoryProvider(
        providers: [
          RepositoryProvider<FeatureFlagRepository>.value(
            value: const FeatureFlagRepository(FeatureFlags({})),
          ),
          RepositoryProvider<TaskRepository>.value(value: taskRepository),
        ],
        child: BlocProvider(
          create: (context) => FeatureFlagCubit(context.read<FeatureFlagRepository>()),
          child: MaterialApp(
            theme: AppTheme.light,
            darkTheme: AppTheme.dark,
            localizationsDelegates: AppLocalizations.localizationsDelegates,
            supportedLocales: AppLocalizations.supportedLocales,
            home: widget,
          ),
        ),
      ),
    );
  }
}

Every subsequent test for a screen that needs a TaskRepository reuses pumpAppWithTasks — it is written once, in test/helpers/, and never redeclared per test file.

5. Router tests: a fresh buildAppRouter() per test

Mirrors the "function, not singleton" rule in .agents/skills/routing/SKILL.md: every router test builds its own GoRouter instance, never sharing one across tests:

// packages/ui/test/router/app_router_test.dart
Future<void> pumpRouter(WidgetTester tester) {
  // A fresh router per test — no shared navigation state can leak
  // between tests.
  final router = buildAppRouter();
  return tester.pumpWidget(
    RepositoryProvider<FeatureFlagRepository>.value(
      value: const FeatureFlagRepository(FeatureFlags({})),
      child: BlocProvider(
        create: (context) => FeatureFlagCubit(context.read<FeatureFlagRepository>()),
        child: MaterialApp.router(
          routerConfig: router,
          theme: AppTheme.light,
          localizationsDelegates: AppLocalizations.localizationsDelegates,
          supportedLocales: AppLocalizations.supportedLocales,
        ),
      ),
    ),
  );
}

testWidgets('starts at the home route', (tester) async {
  await pumpRouter(tester);
  await tester.pumpAndSettle();

  expect(find.byType(HomeScreen), findsOneWidget);
});

A new route's test follows the same shape: build a fresh router, navigate with router.go(AppRoute.taskDetail.path) (or via a widget tap), and assert the expected screen type is found — including a redirect-guard case if the route uses a typed state.extra argument (assert it bounces back to AppRoute.home.path when given a wrong/missing extra).

Where new shared fakes/pumpers go

WhatWhere
Fake*DataProvider for a domain repository testpackages/domain/test/helpers/
Mock*Repository class declaration reused by several Cubit testsinline per test file is fine for a one-line class MockX extends Mock implements X {}; only extract if genuinely shared setup grows around it
A new pumpApp-style widget-test helperpackages/ui/test/helpers/pump_app.dart
A shared Fake* used for registerFallbackValue across several test filespackages/ui/test/helpers/

Never declare a Mock/Fake class inside lib/ in any package (root AGENTS.md sections 12 and 20) — it belongs in test/ as a dev_dependency-only construct.

Common mistakes

  • Mocking the data provider in a Cubit/BLoC test instead of the domain repository — that skips the layer the Cubit actually depends on and couples the test to an implementation detail.
  • A test that calls a method and asserts only that no exception was thrown, with no assertion on the resulting state/value.
  • Redeclaring pumpApp's provider tree inline in a new widget-test file instead of adding a second pumper to pump_app.dart.
  • Sharing one buildAppRouter() instance across multiple testWidgets blocks.
  • Forgetting registerFallbackValue for a non-primitive mocktail argument type, or registering it per test instead of once in setUpAll.
  • Adding mocktail (or any mocking package) to a package's dependencies instead of dev_dependencies.

Checklist

  • Domain: package:test, hand-written Fake*DataProvider, no mocking package.
  • Data: an ephemeral real instance of the chosen stack preferred over a mock; a mocked client only when no ephemeral mode exists.
  • UI Cubit/BLoC: blocTest + mocktail, mocking the domain repository, registerFallbackValue registered for any non-primitive argument.
  • UI widget: uses pumpApp (or a second pumper added to pump_app.dart), not inline provider wiring.
  • Router: a fresh buildAppRouter() built inside the test, not shared.
  • Every test asserts the actual interesting outcome (emitted state, mapped failure, rendered widget) — not just "it ran".
  • fvm dart run tool/quality_gate.dart passes.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.