agentleFS
Ethiel97Ethiel97/flutter-clean-riverpod-skills/skills/flutter-testing/SKILL.md

flutter-testing

Skill10 starsChanged 41 days agoMITView on GitHub

Use when writing or reviewing tests for a Flutter app that uses Riverpod, Result-based repositories and SDKs behind contracts - unit tests for data sources, repositories and notifiers, widget tests with provider overrides, listener tests with recording fakes, golden tests, or deciding between a mock and a fake.

7 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use dart-concurrency and flutter-accessibility.

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.
  • thiennc-tesoglobalSkill

    dart-concurrency

    Design and debug Dart asynchronous infrastructure with Future, Stream, isolates, cancellation, and lifecycle-safe ownership. Use for races, stale results, background CPU work, stream leaks, or async error handling; not for routine form-local validation or submission unless its underlying ordering mechanism is the task.

    Also in 3 projects that use this

  • thiennc-tesoglobalSkill

    flutter-accessibility

    Build or audit Flutter accessibility for semantics, screen readers, focus, keyboard access, text scaling, contrast, target size, and motion preferences. Pair with flutter-responsive-layout when large text causes reflow, clipping, or overflow across constraints.

    Also in 3 projects that use this

The file

We split this skill into the rules it gives an agent

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

  • 0

    Common

    Most of the 7 edited versions we read keep these.

  • 1

    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:

    If you find yourself mocking an SDK inside a feature test, the SDK is not behind a contract (see flutter-sdk-contracts).

    line 32
  2. Distinctive:

    When a feature depends on a library behaviour that is not yours to guarantee (for example, a failed refetch keeps the previous value so …

    line 161
  3. Very specific:

    Arrange with overrides, not with real wiring: If a test needs Dio or Firebase, a contract is missing.

    line 175
  4. Very specific:

    No sleep: Use tester.pump(duration), fake_async, or container.pump().

    line 181
  5. Very specific:

    Dispose: every ProviderContainer (addTearDown).

    line 182
  6. Very specific:

    Verify layer rules separately with check_boundaries.sh from flutter-feature-scaffold.

    line 183

“Common” is measured: kept by at least 6 in 10 of the 7 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 a contract-based Flutter app

The architecture makes testing cheap: every boundary is a contract you can override. Test behaviour at the boundaries, not implementation details.

Layout

test/
├── helpers/            test_container.dart, pump_app.dart, fakes, helpers.dart (barrel)
├── core/               mirrors lib/core
├── features/<feature>/ mirrors lib/features/<feature>; goldens/ beside the test
└── shared/

One test file per source file, named <file>_test.dart. Group by class.

Mock or fake

UseWhen
Recording fake (a hand-written class that implements the contract and stores what it received)Stateful app-owned contracts: AnalyticsService, ErrorTrackingService, NotificationService. Assertions read as "this event was tracked".
mocktail mockA repository or data-source contract with canned answers: when(repo.getDashboard).thenAnswer((_) async => Success(dashboard)).
Fake ApiClientTesting a remote data source: throw an ApiException, assert the Result. Never mock Dio here.
Mock DioOnly in the DioApiClient test itself.

If you find yourself mocking an SDK inside a feature test, the SDK is not behind a contract (see flutter-sdk-contracts).

Helpers (copy from assets/)

  • test_container.dart: createContainer(overrides:) disposes itself and turns retry off. Riverpod 3 retries failing providers with backoff by default, which makes failure assertions slow and flaky.
  • pump_app.dart: tester.pumpApp(widget, overrides:) mounts under the real theme and localizations and returns the container.
  • recording_analytics_service.dart: from the flutter-sdk-contracts assets.

Recipes

1. Data source: failure mapping

test('maps 401 to AuthFailure', () async {
  final api = _FakeApiClient(throws: const ApiResponseException(statusCode: 401));
  final result = await ProfileRemoteDataSourceImpl(api).fetchProfile();
  expect(result.exceptionOrNull(), isA<AuthFailure>());
});

Cover: success, 401/403 → AuthFailure, 422 → ValidationFailure, 5xx → ServerFailure, connection error → NetworkFailure, timeout → NetworkFailure, anything else → UnknownFailure.

2. Repository: model → entity

Fake the data source, return a model, assert the entity. Also assert a failure is passed through unchanged.

3. Notifier

test('loads items from the repository', () async {
  final repo = _MockItemRepository();
  when(repo.getPage).thenAnswer((_) async => Success(page));
  final container = createContainer(
    overrides: [itemRepositoryProvider.overrideWithValue(repo)],
  );

  final state = await container.read(itemsProvider.future);

  expect(state.items, page.items);
});
  • Override the repository contract; data/ is never touched.
  • For autoDispose providers, hold a listener (keepAlive helper) or the provider can be disposed between reads.
  • Replace a dependency notifier with a test subclass: learningProvider.overrideWith(_RecordingLearningNotifier.new).

4. Submission notifier: dispose mid-flight

test('survives disposal while the action is in flight', () async {
  final completer = Completer<Result<String>>();
  final subscription = container.listen(provider, (_, _) {});
  final pending = container.read(provider.notifier).submit(completer.future);

  subscription.close();           // the screen goes away
  await container.pump();
  completer.complete(const Success('late'));

  await expectLater(pending, completion(isA<Success<String, AppFailure>>()));
});

Without the ref.mounted guard this throws after disposal.

5. Widget

testWidgets('shows an error with a retry', (tester) async {
  await tester.pumpApp(
    const ItemsPage(),
    overrides: [itemRepositoryProvider.overrideWithValue(_FailingRepository())],
  );
  await tester.pumpAndSettle();
  expect(find.text('Try again'), findsOneWidget);
});

6. Listener widget

Mount only the listener over an empty child with UncontrolledProviderScope, override the contracts (recording analytics, mock repository), drive the state and assert:

final analytics = RecordingAnalyticsService();
final container = createContainer(overrides: [
  analyticsServiceProvider.overrideWithValue(analytics),
]);
await tester.pumpWidget(UncontrolledProviderScope(
  container: container,
  child: const ItemsListeners(child: SizedBox()),
));

container.read(itemSubmissionProvider.notifier).add();
await tester.pump();

expect(analytics.events.single, isA<ItemChangedEvent>());

End by unmounting (await tester.pumpWidget(const SizedBox())) before the container is disposed.

7. DioApiClient

Mock Dio and assert exception mapping: timeouts and connection errors → ApiConnectionException, bad responses → ApiResponseException with status and code, anything else → ApiUnknownException. Stub the same method the adapter calls (dio.request<Map<String, dynamic>>(...) in the reference adapter).

8. Goldens — assets/golden_test_template.dart

  • Tag them (@Tags(['golden'])) so they can be run or skipped separately.
  • Load one known font; fix the surface size and devicePixelRatio.
  • One golden per visual state, and per shipped locale (copy length changes layout).
  • Generate and compare on one OS in CI; font rendering differs across platforms.
  • Update deliberately: flutter test --tags golden --update-goldens.

9. Pin framework behaviour you rely on

When a feature depends on a library behaviour that is not yours to guarantee (for example, a failed refetch keeps the previous value so pull-to-refresh can report the error without blanking the list), write a small test that asserts it:

expect(state, isA<AsyncError<String>>());
expect(state.hasValue, isTrue);   // failed AND still holds the old value

A package upgrade that drops it then fails a test instead of showing up as vanishing content on a bad connection, far from any code you changed.

Principles

  • Arrange with overrides, not with real wiring. If a test needs Dio or Firebase, a contract is missing.
  • Assert outcomes: state, events recorded, entities returned. Avoid verify(...).called(n) unless the call count is the behaviour.
  • Regression first. A bug fix starts with a failing test that names the bug in a comment: why it failed, not only what was asserted.
  • No sleep. Use tester.pump(duration), fake_async, or container.pump().
  • Dispose every ProviderContainer (addTearDown).
  • Verify layer rules separately with check_boundaries.sh from flutter-feature-scaffold; tests are not the place for import hygiene.

Review checklist

  • Overrides at the contract boundary; no SDK or Dio mocked in feature tests
  • Recording fakes for stateful services, mocks for canned repository answers
  • Retry disabled in test containers; containers disposed
  • Every failure branch of a data source has a test
  • Notifier tests cover load success, load failure, and a mutation failure that keeps state
  • Goldens tagged, one per state and locale, generated on one OS
  • A regression test accompanies each bug fix

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.