agentsclimarketplace

Testing strategy

Skill zakariaf/Flutter-Skills/skills/testing-strategy

General, reusable Flutter engineering skills for AI coding agents — architecture, Riverpod 3.x, testing, persistence, i18n/RTL, accessibility, navigation & more. Agent Skills open standard; works with Claude Code, Cursor, Codex & 70+ agents.

Install
npx -y skills add zakariaf/Flutter-Skills --skill testing-strategy

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • 17 days oldThe repository was created 17 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Enforces test doctrine where shape follows code not the pyramid: pushes logic into Flutter-free packages tested with pure package:test and an injected Clock; asserts invariants with seeded fuzz tests against an independent oracle plus round-trip and rounding goldens; prefers bare-implements fakes over mocktail for code you own; tests the data layer against a real NativeDatabase.memory Drift engine, never a mocked DAO; drives Notifiers headlessly with ProviderContainer overrides; guards one end-to-end acceptance gate and a runtime invariant tripwire; floors coverage on unrecoverable-bug files not a global percentage; and fixes the coverage-lies-upward gap. Use when writing tests under test/ or integration_test/, choosing unit vs widget vs integration, adding a fuzz or round-trip property, wiring a fake or ProviderContainer test, gating coverage, or triaging a flaky-suite failure.

SKILL.md

14.4 KB, ~3.4k tokens by cl100k_base, as published. Nobody here has run it

Testing Strategy

Tests are the only correctness instrument you control at build time: shape the suite to the code, not to a decades-old ratio, and make each test assert behaviour a build either passes or fails. Applies to every test under test/ and integration_test/.

Read the reference for the task at hand:

  • references/test-layers.md — per-layer harness, imports, teardown, edge tables (pure core, in-memory Drift, headless Notifier, integration).
  • references/property-and-fakes.md — fakes-over-mocks, enum-driven fake state, seeded fuzz, independent oracles, absence-of-a-failure-class tests.
  • references/coverage-and-budget.md — file-level floors, the coverage-lies-upward fix, the suite-time budget, and the manual-pass handoff.

Run scripts/check_test_hygiene.sh and scripts/run_tests.sh before a PR.

Golden, RTL, and a11y widget mechanics live in widget-golden-and-a11y-testing; this skill governs everything below the pixel.

Non-negotiable rules

  1. Shape the suite to the code, not the pyramid. The 70/20/10 numbers trace to a 2011 test-size heuristic (its author said they were "pulled out of a hat") and never described Flutter's unit/widget/integration taxonomy. Test at the cheapest tier that can assert the behaviour: anything expressible as f(input) -> output is a unit or property test, never a pumpWidget. Driving pure logic through the widget tree is slower, flakier, and hides which layer broke.
  2. Put business rules in a Flutter-free package and inject a Clock. Domain math lives in pure Dart with zero Flutter/plugin/IO imports, tested with package:test (not flutter_test). The one time type is package:clock's Clock — NEVER DateTime.now(), never a bespoke ClockService. Pure core reads the ambient clock.now(), pinned in tests with withClock(Clock.fixed(t), …); Riverpod/feature code reads the injected clockProvider, overridden with clockProvider.overrideWithValue(Clock.fixed(t)) (seam owned by value-objects-money-and-units). The ban is structural: a pure package declares no Flutter SDK constraint.
  3. Assert invariants, not just examples. Every conversion has a round-trip test (decode(encode(x)) == x) and rounding goldens at half-way/boundary values; every universal claim is a seeded fuzz loop (for (var seed = 0; seed < N; seed++)) or property test, checked against an independent oracle — never the production function under test. Print the generated input in reason: so a failure is its own minimal repro. == on integers only; closeTo(x, 1e-9) on doubles.
  4. Test the data layer against a real in-memory engine. Use NativeDatabase.memory(), never a mocked DAO — a mocked DAO proves nothing about SQL, constraints, indexes, or migrations. Mock repositories only above the data layer. addTearDown(db.close) and close streams synchronously so reactive .watch() streams do not leak "Timer still pending".
  5. Prefer bare-implements fakes over mocks for code you own. A class FakeX implements X (no noSuchMethod superclass) makes an interface change a compile error, models the risk (state, not call-order) as a field, and doubles as the contract's documentation. Reserve mocktail for genuinely external dependencies; never mockito/@GenerateMocks (codegen, no null-safety win).
  6. registerFallbackValue for every custom type passed to any()/captureAny() in setUpAll. mocktail throws at runtime, not compile time, on a missing fallback — this is the single most common mocktail failure.
  7. Drive Notifiers headlessly with ProviderContainer. Override provider dependencies via overrideWith; never pump a widget to test state. Assert on the exposed AsyncValue/state, drive actions through .notifier, and dispose the container in teardown.
  8. Guard one end-to-end acceptance gate. A single realistic scenario asserted to the exact expected result, plus the conservation invariant (parts sum to the whole). It is the test that proves the pieces compose. Back it with a runtime assert tripwire inside the primitive itself (assert(sum(result) == total)) — free in release, catches the bug the moment it happens.
  9. Hold an unrecoverable-bug-files floor by diff-review, not a coverage gate. There is no automated percentage gate — coverage is a published report (ci-pipeline-and-gates owns that). Instead hold a 100% floor on the handful of files (migrations, the money/allocate primitive, the parser with wire-format traps) where a gap is silent data loss — enforced by reading the diff, not by counting lines. A directory or global percentage gate rewards vanity tests and exclusion churn. First fix the coverage-lies-upward gap: flutter test --coverage omits files no test imports, so the number overstates safety.
  10. Never pumpAndSettle() on an indefinite animation (splash, shimmer, spinner) — it hangs on a 10-minute timeout. Use timed pump(Duration) with fakeAsync.
  11. Keep the suite fast, and hand structurally-untestable paths to a manual pass. A suite that costs minutes gets skipped, and a skipped suite is a distrusted one. Anything an emulator cannot reproduce (real audio, OEM device diversity, native surfaces without a Flutter engine) is enumerated in a named manual pre-release pass, not faked green — a green test that proves nothing is worse than an admitted gap.

Tier the test to the code (package:test vs flutter_test)

Pure logic runs under dart test with no widget binding; only UI and plugin-wrapper tests need flutter_test.

// test/domain/money_test.dart — pure Dart, `dart test`, no flutter_test import.
import 'package:test/test.dart';
import 'package:app_core/app_core.dart';

void main() {
  // Round-trip: encoding is lossless across the ISO-4217 exponent range.
  test('minor-units round-trip for exponents 0, 2, 3', () {
    for (final currency in [Currency.jpy, Currency.usd, Currency.kwd]) {
      final money = Money(amountMinor: 12345, currency: currency);
      expect(Money.parse(money.format(), currency), equals(money));
    }
  });
}

Inject the clock; pin it in tests

Production reads clock.now(); tests freeze it so time-dependent logic is deterministic — no wall clock, no flake.

// Production: a Reminder is due when its target time has passed.
bool isDue(Reminder r) => !clock.now().isBefore(r.dueAt);

// test/domain/reminder_test.dart
test('reminder is due exactly at its due instant', () {
  final due = DateTime.utc(2026, 1, 1, 9);
  withClock(Clock.fixed(due), () {
    expect(isDue(Reminder(dueAt: due)), isTrue);
  });
});

For code behind a provider, inject clockProvider and freeze it in a container test with clockProvider.overrideWithValue(Clock.fixed(due)) — same Clock, same fixed instant, driven through Riverpod instead of the ambient zone.

Seeded fuzz against an independent oracle

Twenty lines of package:test give you what a stale property package would, with the generated input as the built-in minimal repro. The oracle must be independent of the function under test.

// test/domain/allocate_test.dart
test('allocate: parts always sum to the input (fuzz)', () {
  final rng = Random(0xC0FFEE);
  for (var seed = 0; seed < 500; seed++) {
    final n = rng.nextInt(20) + 1;
    final total = rng.nextInt(100000);
    final weights = List.generate(n, (_) => rng.nextInt(500));
    final parts = allocate(amountMinor: total, weights: weights);
    expect(parts.fold(0, (a, b) => a + b), equals(total),
        reason: 'seed=$seed total=$total weights=$weights'); // its own repro
  }
});

Real in-memory Drift, never a mocked DAO

// test/data/order_dao_test.dart
void main() {
  late AppDatabase db;
  setUp(() => db = AppDatabase(NativeDatabase.memory()));
  tearDown(() => db.close()); // reactive .watch() streams must not leak timers

  test('CHECK constraint rejects a negative quantity', () {
    expect(
      () => db.orderDao.insertItem(const OrderItem(quantity: -1)),
      throwsA(isA<SqliteException>()), // proves the real constraint, not a stub
    );
  });
}

Headless Notifier tests with ProviderContainer

// test/features/cart/cart_notifier_test.dart
void main() {
  test('addItem publishes the new total', () async {
    final container = ProviderContainer(
      overrides: [orderRepositoryProvider.overrideWith((ref) => _FakeOrderRepo())],
    );
    addTearDown(container.dispose);

    await container.read(cartNotifierProvider.future);
    await container.read(cartNotifierProvider.notifier).addItem(_anItem);

    final state = container.read(cartNotifierProvider);
    expect(state, isA<AsyncData<CartState>>());
    expect(state.requireValue.totalMinor, 1299);
  });
}

ProviderContainer.test() (Riverpod 3.x) auto-disposes and is the preferred form when available; otherwise addTearDown(container.dispose).

Drive timers with fakeAsync

test('debounced search fires once after the window', () {
  fakeAsync((async) {
    final calls = <String>[];
    final debouncer = Debouncer(const Duration(milliseconds: 300));
    debouncer.run(() => calls.add('a'));
    async.elapse(const Duration(milliseconds: 299));
    expect(calls, isEmpty);
    async.elapse(const Duration(milliseconds: 1));
    expect(calls, ['a']);
  });
});

Anti-patterns

  • Inverting the pyramid — driving pure f(input) -> output logic through pumpWidget. Slower, flakier, hides the broken layer.
  • DateTime.now() / ambient Random() reachable from domain logic. Makes "identical inputs -> identical output" untestable; inject Clock and a seed.
  • flutter_test imported into a pure package. Breaks the structural boundary and slows the fastest tier; the package should declare no Flutter SDK constraint.
  • A mocked DAO standing in for the database. Proves nothing about SQL, constraints, indexes, or migrations. Use NativeDatabase.memory().
  • == on doubles, or covering a universal claim with one lucky example instead of a fuzz loop over an independent oracle.
  • A fake that lies about its contract (a dependency that can never fail) — the test then reflects a happy-path stub, not real behaviour. Fakes must expose failure.
  • Missing registerFallbackValue for a custom any() argument — a runtime throw no compiler catches.
  • pumpAndSettle() on an indefinite indicator — hangs on the 10-minute timeout.
  • A global/directory coverage-percentage gate — rewards vanity tests, and the raw number lies upward. Floor the unrecoverable files; publish the rest.
  • A green test over a structurally-untestable path (real audio, native tile, device diversity) — worse than an admitted gap, because it stops anyone checking by hand.
  • A test with no meaningful expect, shared mutable state across tests, or a vague name like test('works').

Definition of done

  • Each test lives at the cheapest tier that can assert it; pure logic under package:test, widget/integration only where the UI/plugin seam is the point.
  • Domain logic reads clock.now(), never DateTime.now(); tests pin it with withClock.
  • Every conversion has a round-trip test and boundary rounding goldens; every universal claim is a seeded fuzz/property test against an independent oracle, with the input echoed in reason:.
  • The data layer is tested against NativeDatabase.memory(), not a mocked DAO; db.close() in teardown.
  • Owned collaborators are bare-implements fakes; mocktail is reserved for external deps with registerFallbackValue for every custom any() type.
  • Notifiers are tested through ProviderContainer overrides, disposed in teardown; no widget pumped to test state.
  • One acceptance gate asserts a realistic scenario exactly and the conservation invariant; the primitive carries a runtime assert tripwire.
  • Coverage floors sit on the unrecoverable-bug files; the coverage-lies-upward gap is fixed (untested files included); generated files stripped.
  • No pumpAndSettle() on an indefinite animation; timers driven by fakeAsync.
  • The suite stays within its time budget; every structurally-untestable path is named in the manual pre-release pass, not faked green.

Related skills

  • See widget-golden-and-a11y-testing for golden lanes, the overflow/RTL matrix, and honest a11y limits.
  • See value-objects-money-and-units for the allocate() primitive and Clock injection this skill tests.
  • See persistence-drift for the DAO/value-object seam the in-memory tests exercise.
  • See run-migration for the forward-only migration ritual whose from→to paths get the 100% floor.
  • See error-handling-typed-results for the sealed Result/Failure the exhaustive-switch tests assert on.
  • See async-safety for the dropped-Future holes the silence-review checklist targets.
  • See service-boundary-and-native for the injectable ports the fakes stand in for.
  • See ci-pipeline-and-gates for how these lanes and greps are wired as gates.

References

Gives 0 of the 12 instructions most test skills give in ~3.4k tokens

Counted across 964 of the 1,571 authors here whose files we hold, read 2026-08-07

  • close the browser when donein 55 of 964, across 12 files
  • wait for network idle statein 51 of 964, across 6 files
  • launch chromium in headless modein 49 of 964, across 6 files
  • use descriptive selectors for elementsin 49 of 964, across 6 files
  • run provided scripts with help flag firstin 49 of 964, across 6 files
  • add appropriate explicit waitsin 48 of 964, across 5 files
  • use bundled scripts as black boxesin 46 of 964, across 3 files
  • do not read script source codein 46 of 964, across 3 files
  • use sync playwright for scriptsin 46 of 964, across 3 files
  • inspect dom before executing actionsin 46 of 964, across 3 files
  • run the full test suitein 37 of 964
  • write the failing test firstin 29 of 964, across 23 files

Said here and by no other author read

  • shape the test suite to the code not the pyramid
  • test business rules in a flutter-free package
  • inject a clock and never use datetime.now
  • assert invariants using seeded fuzz tests and independent oracles
  • add round-trip tests and rounding goldens for conversions
  • test the data layer against a real in-memory database

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.