Skip to content
Back to skills

Flutter Testing

ASecurity

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.

  • 6 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 3, 2026
developmentgotestingapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 4 files and shows the line behind each finding

Scanned October 3, 2026

npx -y skills add Ethiel97/flutter-clean-riverpod-skills --skill flutter-testing --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Flutter Testing?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Flutter Testing
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/ethiel97-flutter-testing/badge)](https://www.skillsdirectory.com/skills/ethiel97-flutter-testing)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: flutter-testing
description: 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.
---

# 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

```text
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

| Use | When |
|---|---|
| **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` mock** | A repository or data-source contract with canned answers: `when(repo.getDashboard).thenAnswer((_) async => Success(dashboard))`. |
| **Fake `ApiClient`** | Testing a remote data source: throw an `ApiException`, assert the `Result`. Never mock Dio here. |
| **Mock `Dio`** | Only 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**

```dart
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**

```dart
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**

```dart
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**

```dart
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:

```dart
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:

```dart
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

Files in this skill

  • SKILL.md7.5 KB
  • assets/golden_test_template.dart2.8 KB
  • assets/pump_app.dart1.3 KB
  • assets/test_container.dart1001 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.