flutter-tester

A comprehensive Flutter testing skill for creating, writing, and analyzing tests in any Flutter project. Provides guidance on test structure, mocking patterns, Riverpod testing, widget testing, and in

flutter
tester

SKILL.md

GitHub

Flutter Tester

Requirements

  • Flutter project with flutter_test dependency
  • Works with Riverpod, Mockito, and GetIt
  • Run dart run build_runner build to generate mocks after adding @GenerateMocks annotations
  • Compatible with FVM (fvm flutter test instead of flutter test)

Overview

Test each architectural layer in isolation using Given-When-Then structure. Always test both success and error paths. Never mock providers — override their dependencies instead.

Reference Files

Load the relevant file based on what you're testing:

What you're testing Reference file
Repository, DAO, Service logic references/layer_testing_patterns.md
Widget UI, interactions, dialogs, navigation references/widget_testing_guide.md
Riverpod provider state, mutations, lifecycle references/riverpod_testing_guide.md

Core Principles

1. Layer Isolation

Test each layer against its own mocked dependencies:

Layer What to test What to mock
Repository Data coordination between sources DAOs, APIs, Logger
DAO Database CRUD operations Use real in-memory DB, mock Logger
Provider State management and transitions Services, Repositories
Service Business logic and workflows Repositories, Network clients
Widget UI behaviour and interactions Provider dependencies (via overrides)

2. Given-When-Then Structure

test('Given valid data, When fetchUsers called, Then returns user list', () async {
  // Arrange (Given)
  when(mockDAO.fetchAll()).thenAnswer((_) async => expectedUsers);

  // Act (When)
  final result = await repository.fetchUsers();

  // Assert (Then)
  expect(result, equals(expectedUsers));
  verify(mockDAO.fetchAll()).called(1);
});

3. Test Organisation

group('UserRepository', () {
  group('fetchUsers', () {
    setUp(() { /* init mocks, register with GetIt */ });
    tearDown(() => GetIt.I.reset()); // Always reset GetIt

    test('Given success ... When ... Then ...', () { });
    test('Given error  ... When ... Then ...', () { });
  });
});

Standard Test Setup

Generate Mocks

@GenerateMocks([IUserDAO, IUserAPI, ILogger])
void main() { ... }

Run dart run build_runner build after modifying @GenerateMocks.

Register with GetIt

setUp(() {
  mockDAO = MockIUserDAO();
  mockLogger = MockILogger();
  GetIt.I
    ..registerSingleton<IUserDAO>(mockDAO)
    ..registerSingleton<ILogger>(mockLogger);
});

tearDown(() => GetIt.I.reset()); // Critical — always reset

Fakes vs Mocks

  • Fakes (class FakeLogger extends ILogger) — silent stubs; use when you don't need to verify calls
  • Mocks (MockILogger) — use when you need when(), verify(), or thenThrow()

Quick Reference

Scenario Key pattern
Test a repository Mock DAO + API → inject into repository constructor
Test a DAO FakeDatabase or openInMemoryDatabase() in setUp, delete table in tearDown
Test a Riverpod provider createContainer(overrides: [serviceProvider.overrideWith(...)])
Test a widget Set screen size, use find.byKey(), call pumpAndSettle()
Test a loading state Use Completer, pump() to assert loading, complete, pump() again
Test platform-specific UI debugDefaultTargetPlatformOverride = TargetPlatform.iOS — reset after
Test GoRouter navigation FakeGoRouter + MockGoRouterProvider

Running Tests

flutter test --coverage                       # All tests with coverage
flutter test test/path/to/test.dart           # Specific file
flutter test --plain-name "Given valid data"  # Filter by name
genhtml coverage/lcov.info -o coverage/html   # Generate HTML coverage report
# Prefix any command with `fvm` if using Flutter Version Manager

Common Mistakes

Mistake Fix
Mocking a provider directly Override its dependencies: provider.overrideWith(...)
Missing GetIt.I.reset() in tearDown Tests pollute each other — always reset
await Future.delayed() in tests Use await tester.pumpAndSettle() or Completer instead
Finding widgets by text string Use find.byKey(const Key('name')) — stable across text changes
No screen size in widget tests Add tester.view.physicalSize = const Size(1000, 1000)
Not resetting debugDefaultTargetPlatformOverride Set to null at the end of the test
tearDown() without a lambda Write tearDown(() async { ... }) not tearDown() async { ... }

Test Checklist

Setup & Mocking:

  • Dependencies mocked (not providers)
  • SharedPreferences mocked if used
  • GetIt.I.reset() in tearDown
  • Streams closed in tearDown
  • Controllers disposed in tearDown

Widget Tests:

  • Keys added to source widgets and used in find.byKey()
  • Screen size set (physicalSize + devicePixelRatio)
  • Platform overrides reset (debugDefaultTargetPlatformOverride = null)
  • Navigation verified if applicable

Test Coverage:

  • Success and failure paths covered
  • Edge cases tested (null, empty, max values)
  • Loading and error states tested
  • Async handled correctly (no Future.delayed)

Code Quality:

  • Given-When-Then naming used
  • verify() or verifyNever() where appropriate
  • Tests are isolated and deterministic