Imported from ChrisTorres404/AICodeAssist (
core/skills/dart-flutter-testing/SKILL.md). Install upstream withnpx skills add ChrisTorres404/AICodeAssist --skill dart-flutter-testing. Copyright stays with the author.
Dart/Flutter Testing Patterns
Reference examples
Unit Tests: State Managers
BLoC with bloc_test
group('CartBloc', () {
late CartBloc bloc;
late MockCartRepository repository;
setUp(() {
repository = MockCartRepository();
bloc = CartBloc(repository);
});
tearDown(() => bloc.close());
blocTest<CartBloc, CartState>(
'emits updated items when CartItemAdded',
build: () => bloc,
act: (b) => b.add(CartItemAdded(testItem)),
expect: () => [CartState(items: [testItem])],
);
blocTest<CartBloc, CartState>(
'emits empty cart when CartCleared',
seed: () => CartState(items: [testItem]),
build: () => bloc,
act: (b) => b.add(CartCleared()),
expect: () => [const CartState()],
);
});
Riverpod with ProviderContainer
test('usersProvider loads users from repository', () async {
final container = ProviderContainer(
overrides: [userRepositoryProvider.overrideWithValue(FakeUserRepository())],
);
addTearDown(container.dispose);
final result = await container.read(usersProvider.future);
expect(result, isNotEmpty);
});
Widget Tests
testWidgets('CartPage shows item count badge', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
cartNotifierProvider.overrideWith(() => FakeCartNotifier([testItem])),
],
child: const MaterialApp(home: CartPage()),
),
);
await tester.pump();
expect(find.text('1'), findsOneWidget);
expect(find.byType(CartItemTile), findsOneWidget);
});
testWidgets('shows empty state when cart is empty', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [cartNotifierProvider.overrideWith(() => FakeCartNotifier([]))],
child: const MaterialApp(home: CartPage()),
),
);
await tester.pump();
expect(find.text('Your cart is empty'), findsOneWidget);
});
Fakes Over Mocks
Prefer hand-written fakes for complex dependencies:
class FakeUserRepository implements UserRepository {
final _users = <String, User>{};
Object? fetchError;
@override
Future<User?> getById(String id) async {
if (fetchError != null) throw fetchError!;
return _users[id];
}
@override
Future<List<User>> getAll() async {
if (fetchError != null) throw fetchError!;
return _users.values.toList();
}
@override
Stream<List<User>> watchAll() => Stream.value(_users.values.toList());
@override
Future<void> save(User user) async {
_users[user.id] = user;
}
@override
Future<void> delete(String id) async {
_users.remove(id);
}
void addUser(User user) => _users[user.id] = user;
}
Async Testing
// Use fake_async for controlling timers and Futures
test('debounce triggers after 300ms', () {
fakeAsync((async) {
final debouncer = Debouncer(delay: const Duration(milliseconds: 300));
var callCount = 0;
debouncer.run(() => callCount++);
expect(callCount, 0);
async.elapse(const Duration(milliseconds: 200));
expect(callCount, 0);
async.elapse(const Duration(milliseconds: 200));
expect(callCount, 1);
});
});
Golden Tests
testWidgets('UserCard golden test', (tester) async {
await tester.pumpWidget(
MaterialApp(home: UserCard(user: testUser)),
);
await expectLater(
find.byType(UserCard),
matchesGoldenFile('goldens/user_card.png'),
);
});
Test Naming
test('returns null when user does not exist', () { ... });
test('throws NotFoundException when id is empty string', () { ... });
testWidgets('disables submit button while form is invalid', (tester) async { ... });
In this pipeline
- Work is a work order: open it with
wo new, size it honestly, fill the SPEC before code. - Verification means behavioural tests that ran:
wo verify <n> --run <suite>writes the status from the exit code.NOT EXECUTED — PLAN ONLYis honest; a typedPASSis not. - Search the packs before building:
pack search "<problem>". - Route by area:
wo new --area,bug new --category; the routing tables are incore/rules/common/.