Testing Patterns
Testing Patterns Reference
Quick-reference for deciding which test style to use, how to structure TestContext classes, and how to name tests. For step-by-step implementation, see the Testing Strategy how-to guide.
Test Style Decision Table
Section titled “Test Style Decision Table”| Layer | Component | Test Style | Group By |
|---|---|---|---|
| Datasource | Model Convertors | Standard unit | Input/output cases |
| Datasource | Filter Convertors | Standard unit | Input/output cases |
| Datasource | Request Convertors | Standard unit | Input/output cases |
| Datasource | CacheHandlerSpecs | Standard unit | Handler behavior |
| Datasource | StreamConvertors | Standard unit | Stream output cases |
| Domain | Repository (integration) | BDD | Business scenario |
| Services | Orchestrator / Cross-cutting | BDD | Business scenario |
| UI | Notifiers (simple CRUD) | Standard unit | State transition |
| UI | Notifiers (complex flow) | BDD | Business scenario |
| UI | Widgets (user scenarios) | BDD | User journey |
| UI | Widgets (rendering only) | Standard unit | Visual output |
Test Context Structure
Section titled “Test Context Structure”Use for any test file with more than 3 tests.
class [ClassUnderTest]TestContext { [ClassUnderTest]TestContext() { container = ProviderContainer(overrides: [ /* mock overrides */ ]); }
late final ProviderContainer container;
// 1. Mock declarations final mockRepo = MockRepository();
// 2. Stub methods — one per scenario setup void stubSuccess(List<Model> data) { ... } void stubFailure(Exception error) { ... }
// 3. Accessor — the class under test ClassUnderTest get sut => container.read(provider.notifier);
// 4. Static factories — sample data static List<Model> sampleData([int count = 3]) => ...;
// 5. Assertion helpers void verifyRepoAccessed({int times = 1}) { ... }
// 6. Cleanup void dispose() => container.dispose();}BDD Test Name Format
Section titled “BDD Test Name Format”'Given [precondition], ''When [action], ''Then [observable outcome]'Examples:
test('Given events and attendees exist, ' 'When getDashboard is called, ' 'Then it returns a merged dashboard from both repos', () async { ... });
test('Given the event repository fails, ' 'When getDashboard is called, ' 'Then it propagates the error without calling attendee repo', () async { ... });
testWidgets('Given the user sees an error, ' 'When they tap Retry, ' 'Then the list reloads and shows data', (tester) async { ... });Table-Driven Test Format
Section titled “Table-Driven Test Format”Use when a component has exhaustive state transitions.
class [Component]StateCase { const [Component]StateCase({ required this.description, required this.arrange, required this.act, required this.expectedState, });
final String description; final void Function(TestContext ctx) arrange; final Future<void> Function(Component component) act; final bool Function(State state) expectedState;}
final cases = [ [Component]StateCase( description: 'scenario A -> expected result', arrange: (ctx) => ctx.stubScenarioA(), act: (component) => component.doAction(), expectedState: (state) => state.isExpectedResult, ), // ... more cases];
for (final c in cases) { test(c.description, () async { final ctx = TestContext(); c.arrange(ctx); await c.act(ctx.sut); expect(c.expectedState(ctx.state), isTrue, reason: 'Failed: ${c.description}'); ctx.dispose(); });}Standard Unit Test Naming
Section titled “Standard Unit Test Naming”For technical components, test names describe input and expected output:
// Convertor teststest('converts GQL data to domain model with all fields', () { ... });test('maps null fields to empty string defaults', () { ... });test('builds request with pagination parameters', () { ... });
// CacheHandlerSpecs teststest('upsert handler writes updated data to cache', () { ... });test('delete handler removes item from cached list', () { ... });
// Simple notifier teststest('loads attendees from repository on init', () { ... });test('sets error state when repository throws', () { ... });Anti-Patterns
Section titled “Anti-Patterns”| Anti-Pattern | Why It’s Wrong | Do This Instead |
|---|---|---|
| Coverage percentage targets | Creates incentive to test trivial code, miss critical paths | Write tests that prove business invariants |
| Group by method name for domain code | Fragments related scenarios across groups | Group by behavior scenario |
| Scattered setUp/tearDown | Duplicated mock setup, hard to maintain | Use TestContext classes |
| Arrange/Act/Assert comments in BDD | Redundant with Given/When/Then naming | Let the test name provide structure |
| Testing internal state | Breaks on refactor, doesn’t prove user value | Assert observable behavior and outputs |