Loading...
Loading...
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.
npx skill4agent add zakariaf/flutter-skills testing-strategytest/integration_test/references/test-layers.mdreferences/property-and-fakes.mdreferences/coverage-and-budget.mdscripts/check_test_hygiene.shscripts/run_tests.shwidget-golden-and-a11y-testingf(input) -> outputpumpWidgetClockpackage:testflutter_testpackage:clockClockDateTime.now()ClockServicecoreclock.now()withClock(Clock.fixed(t), …)clockProviderclockProvider.overrideWithValue(Clock.fixed(t))value-objects-money-and-unitsdecode(encode(x)) == xfor (var seed = 0; seed < N; seed++)reason:==closeTo(x, 1e-9)NativeDatabase.memory()addTearDown(db.close).watch()implementsclass FakeX implements XnoSuchMethodmocktailmockito@GenerateMocksregisterFallbackValueany()captureAny()setUpAllProviderContaineroverrideWithAsyncValue.notifierassert(sum(result) == total)ci-pipeline-and-gatesflutter test --coveragepumpAndSettle()pump(Duration)fakeAsyncpackage:testflutter_testdart testflutter_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));
}
});
}clock.now()// 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);
});
});clockProviderclockProvider.overrideWithValue(Clock.fixed(due))Clockpackage: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
}
});// 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
);
});
}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()addTearDown(container.dispose)fakeAsynctest('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']);
});
});f(input) -> outputpumpWidgetDateTime.now()Random()Clockflutter_testNativeDatabase.memory()==registerFallbackValueany()pumpAndSettle()expecttest('works')package:testclock.now()DateTime.now()withClockreason:NativeDatabase.memory()db.close()implementsmocktailregisterFallbackValueany()ProviderContainerassertpumpAndSettle()fakeAsyncwidget-golden-and-a11y-testingvalue-objects-money-and-unitsallocate()Clockpersistence-driftrun-migrationerror-handling-typed-resultsResultFailureasync-safetyservice-boundary-and-nativeci-pipeline-and-gatesseeded-determinism-and-golden-vectorspackage:clockpackage:fake_asyncpackage:mocktail