# Flutter Testing Best Practices Guide - 2025

> BuyerKiosk Live Flutter App - Production-Grade Testing Patterns for Flutter 3.38.3, Riverpod 3.x, and Freezed 3.x

## Executive Summary

This guide provides actionable testing strategies for your Flutter 3.38.3 app with Riverpod 3.x AsyncNotifier providers and Freezed data models. The recommendations are based on 2025 industry best practices and real production patterns from Riverpod projects at scale.

**Key Principles:**
- Balanced testing pyramid: Many unit tests, reasonable widget tests, selective integration tests
- Riverpod 3.x specific patterns using `ProviderContainer.test()` and listener verification
- Mocktail (already in your pubspec) for clean, code-generation-free mocking
- GoRouter navigation testing with URL and action verification
- CI-integrated coverage tracking with Codecov

---

## 1. Testing Framework Overview

### Three Testing Types in Flutter

| Type | Scope | Speed | Cost | Coverage % |
|------|-------|-------|------|-----------|
| **Unit** | Single function/class | < 100ms | Low maintenance | 30-40% |
| **Widget** | Single widget/screen | 100-500ms | Medium | 30-40% |
| **Integration** | Full app flows | 1-10s | High maintenance | 20-30% |

**Recommendation for BuyerKiosk:**
- **50-60% Unit Tests**: Provider logic, mappers, utility functions, error handling
- **30-35% Widget Tests**: Screens, dialogs, navigation, user interactions
- **5-10% Integration Tests**: Auth flow, critical user journeys (login → store → notes)

### Test Execution
```bash
# Run all tests
flutter test

# Run with coverage
flutter test --coverage

# Run specific test file
flutter test test/domain/repositories/store_repository_test.dart

# Run tests matching pattern
flutter test -k "provider"

# Watch mode (rerun on file changes)
flutter test --watch
```

---

## 2. Test Organization & Structure

### Recommended Directory Layout

```
test/
├── core/                           # Core layer tests
│   ├── network/
│   │   └── api_client_test.dart
│   ├── services/
│   │   └── push_notification_service_test.dart
│   ├── theme/
│   │   └── app_theme_test.dart
│   └── utils/
│       └── formatter_test.dart
│
├── data/                           # Data layer tests
│   ├── models/
│   │   ├── store_model_test.dart
│   │   └── mappers/
│   │       ├── store_mapper_test.dart
│   │       └── workbook_note_mapper_test.dart
│   └── datasources/
│       ├── store_local_datasource_test.dart
│       └── store_remote_datasource_test.dart
│
├── domain/                         # Domain layer tests
│   └── repositories/
│       └── store_repository_test.dart
│
├── presentation/                   # Presentation layer tests
│   ├── providers/
│   │   ├── dashboard_provider_test.dart
│   │   ├── store_detail_provider_test.dart
│   │   └── today_tasks_provider_test.dart
│   │
│   ├── screens/
│   │   ├── dashboard_screen_test.dart
│   │   ├── store_detail_screen_test.dart
│   │   └── workbook_notes_screen_test.dart
│   │
│   └── widgets/
│       ├── error_widget_test.dart
│       └── metric_card_test.dart
│
├── router/
│   └── app_router_test.dart
│
└── fixtures/                       # Shared test data
    ├── store_fixtures.dart
    ├── queue_item_fixtures.dart
    └── mock_factories.dart
```

### Naming Conventions

```
✅ GOOD
- dashboard_provider_test.dart    # What is being tested
- store_detail_screen_test.dart
- test('loads dashboard on initial build', () {})
- test('displays error when API key is invalid', () {})

❌ BAD
- test.dart                         # Too vague
- provider_test.dart                # Unclear which provider
- test('test loading', () {})       # Unclear what's being tested
```

---

## 3. Unit Testing Best Practices

### 3.1 Testing Riverpod Providers (AsyncNotifier)

**Key Pattern: `ProviderContainer.test()` with Listeners**

```dart
// test/presentation/providers/dashboard_provider_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mocktail/mocktail.dart';
import 'package:buyer_kiosk_live/presentation/providers/dashboard_provider.dart';
import 'package:buyer_kiosk_live/domain/entities/store.dart';

// Mock the repository
class MockStoreRepository extends Mock implements StoreRepository {}

void main() {
  group('DashboardProvider', () {
    late MockStoreRepository mockRepository;

    setUpAll(() {
      // Register fallback values for complex types
      registerFallbackValue(const AsyncLoading<List<Store>>());
    });

    setUp(() {
      mockRepository = MockStoreRepository();
    });

    test('loads stores successfully on initial build', () async {
      // Arrange: Setup mock to return test data
      final stores = [
        const Store(
          typeNum: 'bk01',
          storeName: 'Test Store',
          // ... other required fields
        ),
      ];
      when(() => mockRepository.getAllStores())
          .thenAnswer((_) async => stores);

      // Act: Create container with override
      final container = ProviderContainer(
        overrides: [
          storeRepositoryProvider.overrideWithValue(mockRepository),
        ],
      );
      addTearDown(container.dispose);

      // Use listener to verify state transitions
      final listener = Listener<AsyncValue<List<Store>>>();
      container.listen(dashboardProvider, listener);

      // Verify initial AsyncLoading state
      verify(() => listener(null, any(that: isA<AsyncLoading>())))
          .called(1);

      // Act: Wait for async operation
      await Future<void>.value();
      await container.pump(); // Pump to allow async completion

      // Assert: Verify successful load
      expect(container.read(dashboardProvider).value, equals(stores));
      verify(() => listener(
        any(that: isA<AsyncLoading>()),
        any(that: isA<AsyncData<List<Store>>>()),
      )).called(1);
    });

    test('handles error when API fails', () async {
      // Arrange
      when(() => mockRepository.getAllStores())
          .thenThrow(Exception('API Error'));

      final container = ProviderContainer(
        overrides: [
          storeRepositoryProvider.overrideWithValue(mockRepository),
        ],
      );
      addTearDown(container.dispose);

      final listener = Listener<AsyncValue<List<Store>>>();
      container.listen(dashboardProvider, listener);

      // Act: Wait for async completion
      await Future<void>.value();
      await container.pump();

      // Assert: Verify error state
      final state = container.read(dashboardProvider);
      expect(state, isA<AsyncError<List<Store>>>());
    });

    test('refresh reloads dashboard data', () async {
      // Arrange
      final stores = [const Store(typeNum: 'bk01', storeName: 'Test')];
      when(() => mockRepository.getAllStores())
          .thenAnswer((_) async => stores);

      final container = ProviderContainer(
        overrides: [
          storeRepositoryProvider.overrideWithValue(mockRepository),
        ],
      );
      addTearDown(container.dispose);

      final listener = Listener<AsyncValue<List<Store>>>();
      container.listen(dashboardProvider, listener);

      // Wait for initial load
      await Future<void>.value();
      await container.pump();

      // Act: Trigger refresh
      await container.read(dashboardProvider.notifier).refresh();

      // Assert: Repository was called again
      verify(() => mockRepository.getAllStores()).called(2);
    });
  });
}
```

### 3.2 Testing Family Providers (with Parameters)

```dart
// test/presentation/providers/store_detail_provider_test.dart
group('StoreDetailProvider (family)', () {
  test('loads specific store details', () async {
    const typeNum = 'bk01';

    final stores = [
      const StoreDetail(
        typeNum: typeNum,
        storeName: 'Store One',
        currentQueue: 5,
        // ... other fields
      ),
    ];

    when(() => mockRepository.getStoreDetail(typeNum))
        .thenAnswer((_) async => stores.first);

    final container = ProviderContainer(
      overrides: [
        storeDetailRepositoryProvider.overrideWithValue(mockRepository),
      ],
    );
    addTearDown(container.dispose);

    final listener = Listener<AsyncValue<StoreDetail>>();
    // Use family modifier with parameter
    container.listen(storeDetailProvider(typeNum), listener);

    await Future<void>.value();
    await container.pump();

    expect(
      container.read(storeDetailProvider(typeNum)).value?.typeNum,
      equals(typeNum),
    );
  });

  test('different parameters maintain separate cache', () async {
    when(() => mockRepository.getStoreDetail(any()))
        .thenAnswer((_) async => const StoreDetail(typeNum: 'bk01'));

    final container = ProviderContainer(
      overrides: [
        storeDetailRepositoryProvider.overrideWithValue(mockRepository),
      ],
    );
    addTearDown(container.dispose);

    // Read different family instances
    final bk01 = container.read(storeDetailProvider('bk01'));
    final bk02 = container.read(storeDetailProvider('bk02'));

    // Should be different providers
    expect(identical(bk01, bk02), isFalse);
  });
});
```

### 3.3 Testing Mapper Functions

```dart
// test/data/models/mappers/store_mapper_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:buyer_kiosk_live/data/models/mappers/store_mapper.dart';
import 'package:buyer_kiosk_live/data/models/store_model.dart';

void main() {
  group('StoreMapper', () {
    test('maps StoreModel to Store entity correctly', () {
      // Arrange
      const model = StoreModel(
        typeNum: 'bk01',
        storeName: 'Test Store',
        currentQueue: 5,
        completedToday: 120,
      );

      // Act
      final entity = model.toDomain();

      // Assert
      expect(entity.typeNum, equals('bk01'));
      expect(entity.storeName, equals('Test Store'));
      expect(entity.currentQueue, equals(5));
      expect(entity.completedToday, equals(120));
    });

    test('handles null optional fields gracefully', () {
      const model = StoreModel(
        typeNum: 'bk01',
        storeName: 'Test',
        currentQueue: null,
        completedToday: null,
      );

      final entity = model.toDomain();

      expect(entity.currentQueue, equals(0)); // Default value
      expect(entity.completedToday, equals(0));
    });
  });
}
```

### 3.4 Testing Error Handling

```dart
// test/presentation/providers/error_handling_test.dart
group('Error Handling in Providers', () {
  test('caches auth errors for 30 seconds', () async {
    when(() => mockRepository.getDashboard())
        .thenThrow(UnauthorizedException('Invalid API key'));

    final container = ProviderContainer(
      overrides: [
        dashboardRepositoryProvider.overrideWithValue(mockRepository),
      ],
    );
    addTearDown(container.dispose);

    // First call triggers error
    await container.read(dashboardProvider).whenData((_) {});
    expect(container.read(dashboardProvider), isA<AsyncError>());

    // Verify error is cached - next call shouldn't hit repository
    await container.read(dashboardProvider).whenData((_) {});
    verifyNever(() => mockRepository.getDashboard());
  });

  test('maps DioException to AppException correctly', () async {
    final dioError = DioException(
      requestOptions: RequestOptions(path: '/api/test'),
      response: Response(
        requestOptions: RequestOptions(path: '/api/test'),
        statusCode: 403,
      ),
    );

    when(() => mockRepository.getDashboard())
        .thenThrow(dioError);

    final container = ProviderContainer(
      overrides: [
        dashboardRepositoryProvider.overrideWithValue(mockRepository),
      ],
    );
    addTearDown(container.dispose);

    await container.read(dashboardProvider).whenData((_) {});

    final state = container.read(dashboardProvider);
    expect(state, isA<AsyncError>());
    // Verify error was properly converted to domain exception
  });
});
```

### 3.5 Test Fixtures for DRY Tests

```dart
// test/fixtures/store_fixtures.dart
import 'package:buyer_kiosk_live/data/models/store_model.dart';
import 'package:buyer_kiosk_live/domain/entities/store.dart';

class StoreFixtures {
  // Model fixtures
  static const storeModel = StoreModel(
    typeNum: 'bk01',
    storeName: 'Platos Closet - Downtown',
    currentQueue: 12,
    completedToday: 145,
    averageTime: 4.5,
  );

  static List<StoreModel> storeListModels() => [
    storeModel,
    const StoreModel(
      typeNum: 'bk02',
      storeName: 'Platos Closet - Mall',
      currentQueue: 8,
      completedToday: 98,
    ),
  ];

  // Entity fixtures
  static const storeEntity = Store(
    typeNum: 'bk01',
    storeName: 'Platos Closet - Downtown',
    currentQueue: 12,
    completedToday: 145,
    averageTime: 4.5,
  );

  static List<Store> storeListEntities() =>
      storeListModels().map((m) => m.toDomain()).toList();
}

// Usage in tests:
void main() {
  test('loads stores', () {
    when(() => mockRepository.getAllStores())
        .thenAnswer((_) async => StoreFixtures.storeListEntities());
    // ...
  });
}
```

---

## 4. Widget Testing Best Practices

### 4.1 Basic Widget Test Structure

```dart
// test/presentation/screens/dashboard_screen_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mocktail/mocktail.dart';
import 'package:buyer_kiosk_live/presentation/screens/dashboard/dashboard_screen.dart';

class MockDashboardProvider extends Mock
    implements AutoDisposeFutureProvider<List<Store>> {}

void main() {
  group('DashboardScreen Widget Tests', () {
    late WidgetTester tester;

    setUp(() {
      // Reset mocks before each test
    });

    testWidgets('displays stores list when data loads', (tester) async {
      // Arrange: Create test app with overridden providers
      await tester.pumpWidget(
        ProviderScope(
          overrides: [
            dashboardProvider.overrideWithValue(
              const AsyncValue.data([
                Store(
                  typeNum: 'bk01',
                  storeName: 'Test Store',
                  currentQueue: 5,
                  completedToday: 120,
                ),
              ]),
            ),
          ],
          child: const MaterialApp(
            home: DashboardScreen(),
          ),
        ),
      );

      // Act: Wait for widgets to build
      await tester.pumpAndSettle();

      // Assert: Verify store appears
      expect(find.text('Test Store'), findsOneWidget);
      expect(find.text('5'), findsOneWidget); // Current queue
    });

    testWidgets('displays loading indicator while fetching', (tester) async {
      // Arrange
      await tester.pumpWidget(
        ProviderScope(
          overrides: [
            dashboardProvider.overrideWithValue(
              const AsyncValue.loading(),
            ),
          ],
          child: const MaterialApp(
            home: DashboardScreen(),
          ),
        ),
      );

      // Act: Initial frame
      await tester.pump();

      // Assert: Loading indicator visible
      expect(find.byType(CircularProgressIndicator), findsOneWidget);
    });

    testWidgets('displays error message on failure', (tester) async {
      // Arrange
      await tester.pumpWidget(
        ProviderScope(
          overrides: [
            dashboardProvider.overrideWithValue(
              AsyncValue.error(
                Exception('API Error'),
                StackTrace.current,
              ),
            ),
          ],
          child: const MaterialApp(
            home: DashboardScreen(),
          ),
        ),
      );

      // Act
      await tester.pumpAndSettle();

      // Assert
      expect(find.byType(ErrorDisplay), findsOneWidget);
      expect(find.text('API Error'), findsOneWidget);
    });

    testWidgets('navigates to store detail on tap', (tester) async {
      // Arrange
      final mockGoRouter = MockGoRouter();

      await tester.pumpWidget(
        ProviderScope(
          overrides: [
            dashboardProvider.overrideWithValue(
              const AsyncValue.data([
                Store(typeNum: 'bk01', storeName: 'Test Store'),
              ]),
            ),
            goRouterProvider.overrideWithValue(mockGoRouter),
          ],
          child: MaterialApp.router(
            routerConfig: mockGoRouter,
          ),
        ),
      );

      await tester.pumpAndSettle();

      // Act: Tap store card
      await tester.tap(find.byType(StoreCard));
      await tester.pumpAndSettle();

      // Assert: Navigation called
      verify(() => mockGoRouter.push('/store/bk01')).called(1);
    });

    testWidgets('pull-to-refresh refreshes stores', (tester) async {
      // Arrange
      final mockNotifier = MockDashboardNotifier();

      await tester.pumpWidget(
        ProviderScope(
          overrides: [
            dashboardProvider.overrideWithValue(
              const AsyncValue.data([
                Store(typeNum: 'bk01', storeName: 'Store'),
              ]),
            ),
            // Override notifier for refresh method
          ],
          child: const MaterialApp(home: DashboardScreen()),
        ),
      );

      await tester.pumpAndSettle();

      // Act: Perform pull-to-refresh
      await tester.drag(
        find.byType(RefreshIndicator),
        const Offset(0, 300),
      );
      await tester.pumpAndSettle();

      // Assert: Data reloaded
      verify(() => mockNotifier.refresh()).called(1);
    });
  });
}
```

### 4.2 Testing Complex Widgets (Notes with Comments)

```dart
// test/presentation/widgets/workbook_note_card_test.dart
testWidgets('note card shows reactions count', (tester) async {
  await tester.pumpWidget(
    MaterialApp(
      home: Scaffold(
        body: WorkbookNoteCard(
          note: const WorkbookNote(
            id: 1,
            title: 'Important Update',
            content: 'Please review the new process',
            reactions: {'like': 5, 'heart': 3},
            createdBy: 'John Doe',
            createdAt: '2025-12-05',
          ),
          onEdit: () {},
          onDelete: () {},
        ),
      ),
    ),
  );

  await tester.pumpAndSettle();

  // Verify reaction count displays
  expect(find.text('5'), findsWidgets); // Like count
  expect(find.text('3'), findsWidgets); // Heart count
});

testWidgets('swipe right triggers edit action', (tester) async {
  final onEditCalled = <void>[];

  await tester.pumpWidget(
    MaterialApp(
      home: Scaffold(
        body: WorkbookNoteCard(
          note: const WorkbookNote(id: 1, title: 'Test'),
          onEdit: () => onEditCalled.add(null),
          onDelete: () {},
        ),
      ),
    ),
  );

  // Act: Swipe right
  await tester.drag(
    find.byType(WorkbookNoteCard),
    const Offset(300, 0),
  );
  await tester.pumpAndSettle();

  // Assert: Edit action triggered
  expect(onEditCalled.length, equals(1));
});
```

### 4.3 Testing GoRouter Navigation

```dart
// test/router/app_router_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
import 'package:buyer_kiosk_live/router/app_router.dart';

extension PumpApp on WidgetTester {
  Future<void> pumpRealRouterApp(
    GoRouter router,
    String initialLocation,
  ) async {
    await pumpWidget(
      MaterialApp.router(
        routerConfig: router,
      ),
    );

    // Navigate to initial location
    router.go(initialLocation);
    await pumpAndSettle();
  }
}

void main() {
  group('GoRouter Navigation', () {
    late GoRouter router;

    setUp(() {
      router = createRouter(initialLocation: '/');
    });

    testWidgets('navigates to dashboard screen', (tester) async {
      await tester.pumpRealRouterApp(router, '/');
      expect(find.byType(DashboardScreen), findsOneWidget);
    });

    testWidgets('navigates to store detail with parameter', (tester) async {
      await tester.pumpRealRouterApp(router, '/store/bk01');
      expect(find.byType(StoreDetailScreen), findsOneWidget);
    });

    testWidgets('redirects to installation when no API key', (tester) async {
      // Setup: No API key in secure storage
      when(() => mockSecureStorage.read(key: 'api_key'))
          .thenAnswer((_) async => null);

      await tester.pumpRealRouterApp(router, '/');

      // Should redirect to installation
      expect(find.byType(InstallationScreen), findsOneWidget);
    });

    testWidgets('navigates through nested routes', (tester) async {
      // Navigate: Dashboard → Store Detail → Notes
      await tester.pumpRealRouterApp(router, '/');
      expect(find.byType(DashboardScreen), findsOneWidget);

      router.push('/store/bk01');
      await tester.pumpAndSettle();
      expect(find.byType(StoreDetailScreen), findsOneWidget);

      router.push('/store/bk01/notes');
      await tester.pumpAndSettle();
      expect(find.byType(WorkbookNotesScreen), findsOneWidget);
    });
  });
}
```

### 4.4 Test App Helper

```dart
// test/helpers/test_app.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';

class TestApp extends StatelessWidget {
  const TestApp({
    required this.home,
    this.overrides = const [],
    this.goRouter,
    Key? key,
  }) : super(key: key);

  final Widget home;
  final List<Override> overrides;
  final GoRouter? goRouter;

  @override
  Widget build(BuildContext context) {
    return ProviderScope(
      overrides: overrides,
      child: goRouter != null
          ? MaterialApp.router(
              routerConfig: goRouter!,
              theme: AppTheme.lightTheme,
            )
          : MaterialApp(
              home: home,
              theme: AppTheme.lightTheme,
            ),
    );
  }
}

// Usage:
testWidgets('my widget test', (tester) async {
  await tester.pumpWidget(
    TestApp(
      home: const MyScreen(),
      overrides: [
        dashboardProvider.overrideWithValue(
          const AsyncValue.data([]),
        ),
      ],
    ),
  );
  // ...
});
```

---

## 5. Mocktail Best Practices

### 5.1 Setting Up Mocks

```dart
// test/fixtures/mock_factories.dart
import 'package:mocktail/mocktail.dart';
import 'package:buyer_kiosk_live/domain/repositories/store_repository.dart';
import 'package:buyer_kiosk_live/core/network/api_client.dart';

// Create mock classes
class MockStoreRepository extends Mock implements StoreRepository {}
class MockApiClient extends Mock implements ApiClient {}

// Register fallback values for complex types
void setUpAllMocks() {
  setUpAll(() {
    // Riverpod AsyncValue types
    registerFallbackValue(const AsyncLoading<List<Store>>());
    registerFallbackValue(const AsyncLoading<StoreDetail>());
    registerFallbackValue(const AsyncData<List<Store>>([]));

    // API types
    registerFallbackValue(RequestOptions(path: '/test'));

    // Custom entities
    registerFallbackValue(const Store(
      typeNum: 'test',
      storeName: 'Test',
    ));
  });
}

// Reusable when/then patterns
extension MockRepositoryExtension on MockStoreRepository {
  void stubGetAllStoresSuccess([List<Store>? stores]) {
    when(() => getAllStores()).thenAnswer(
      (_) async => stores ?? StoreFixtures.storeListEntities(),
    );
  }

  void stubGetAllStoresFailure(Exception exception) {
    when(() => getAllStores()).thenThrow(exception);
  }
}
```

### 5.2 Verification Patterns

```dart
group('Mocktail Verification', () {
  test('repository method called once', () {
    // Arrange & Act
    final result = await container.read(dashboardProvider);

    // Assert: Verify called exactly once
    verify(() => mockRepository.getAllStores()).called(1);
  });

  test('repository method never called', () {
    // Assert: Verify method was never called
    verifyNever(() => mockRepository.getAllStores());
  });

  test('verify call order matters', () {
    // Verify multiple calls in specific order
    verifyInOrder([
      () => mockRepository.getAllStores(),
      () => mockRepository.getStoreDetail('bk01'),
    ]);
  });

  test('verify any method on mock', () {
    // Verify at least one method was called
    verifyNoMoreInteractions(mockRepository);
  });

  test('capture argument for inspection', () {
    final captured = <String>[];

    when(() => mockRepository.getStoreDetail(captureAny()))
        .thenAnswer((_) async => const StoreDetail(typeNum: 'bk01'));

    await mockRepository.getStoreDetail('bk01');
    await mockRepository.getStoreDetail('bk02');

    // Captured arguments: ['bk01', 'bk02']
    expect(captured, equals(['bk01', 'bk02']));
  });
});
```

### 5.3 Using `any()` with Matchers

```dart
group('Mocktail Matchers', () {
  test('match any value', () {
    when(() => mockRepository.getStoreDetail(any()))
        .thenAnswer((_) async => const StoreDetail(typeNum: 'bk01'));

    final result1 = await mockRepository.getStoreDetail('bk01');
    final result2 = await mockRepository.getStoreDetail('bk02');

    // Both return same result because any() matches
    expect(result1.typeNum, equals('bk01'));
    expect(result2.typeNum, equals('bk01'));
  });

  test('match with specific type', () {
    when(() => mockRepository.getStoreDetail(
      any(that: isA<String>()),
    )).thenAnswer((_) async => const StoreDetail(typeNum: 'bk01'));

    // This works
    await mockRepository.getStoreDetail('bk01');
  });

  test('match with predicate', () {
    when(() => mockRepository.getStoreDetail(
      any(that: matches(RegExp(r'^bk\d+$'))),
    )).thenAnswer((_) async => const StoreDetail(typeNum: 'bk01'));

    // This works
    await mockRepository.getStoreDetail('bk01');
  });
});
```

### 5.4 Advanced: Answering with Side Effects

```dart
test('mock increments counter on each call', () {
  var callCount = 0;

  when(() => mockRepository.getAllStores())
      .thenAnswer((_) async {
    callCount++;
    return [];
  });

  await mockRepository.getAllStores();
  await mockRepository.getAllStores();

  expect(callCount, equals(2));
});

test('mock throws on specific argument', () {
  when(() => mockRepository.getStoreDetail('invalid'))
      .thenThrow(NotFoundException());

  when(() => mockRepository.getStoreDetail(any(
    that: isNot('invalid'),
  ))).thenAnswer((_) async => const StoreDetail(typeNum: 'bk01'));

  expect(
    () => mockRepository.getStoreDetail('invalid'),
    throwsA(isA<NotFoundException>()),
  );
});
```

---

## 6. Integration Testing (Selective)

### 6.1 Auth Flow Integration Test

```dart
// test/integration/auth_flow_integration_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:buyer_kiosk_live/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('Authentication Flow Integration Test', () {
    testWidgets('complete login and navigate to dashboard',
        (WidgetTester tester) async {
      // Start app
      app.main();
      await tester.pumpAndSettle();

      // Should show installation screen (no API key)
      expect(find.byType(InstallationScreen), findsOneWidget);

      // Enter API key
      await tester.enterText(
        find.byType(TextField),
        'valid-api-key-here',
      );
      await tester.tap(find.byType(ElevatedButton));
      await tester.pumpAndSettle();

      // Should navigate to dashboard
      expect(find.byType(DashboardScreen), findsOneWidget);

      // Dashboard should load stores
      expect(find.byType(StoreCard), findsWidgets);
    });

    testWidgets('invalid API key shows error', (tester) async {
      app.main();
      await tester.pumpAndSettle();

      // Enter invalid key
      await tester.enterText(
        find.byType(TextField),
        'invalid-key',
      );
      await tester.tap(find.byType(ElevatedButton));
      await tester.pumpAndSettle();

      // Should show error
      expect(find.byType(ErrorDisplay), findsOneWidget);
    });
  });
}
```

### 6.2 Running Integration Tests

```bash
# Run integration tests on Android emulator
flutter test integration_test/auth_flow_integration_test.dart \
  -d emulator-5554

# Run on iOS simulator
flutter test integration_test/auth_flow_integration_test.dart \
  -d macos

# Run all integration tests
flutter test integration_test/

# Generate report
flutter test integration_test/ --coverage
```

---

## 7. Test Coverage & CI Integration

### 7.1 Generate Coverage Locally

```bash
# Run tests with coverage
flutter test --coverage

# Generate HTML report
genhtml coverage/lcov.info -o coverage/html

# Open in browser
open coverage/html/index.html
```

### 7.2 GitHub Actions Integration

```yaml
# .github/workflows/test.yml
name: Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.38.3'

      - name: Install dependencies
        run: flutter pub get

      - name: Run tests
        run: flutter test --coverage

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          file: ./coverage/lcov.info
          flags: unittests
          name: codecov-umbrella

  analyze:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.38.3'

      - name: Install dependencies
        run: flutter pub get

      - name: Analyze code
        run: flutter analyze
```

### 7.3 Coverage Targets

Set coverage targets in your CI/CD pipeline:

```bash
# Check minimum coverage (example: 80%)
lcov --summary coverage/lcov.info | grep -oP 'lines\.\.\.: \K[0-9.]+' | \
  awk '{if ($1 < 80) exit 1}'
```

---

## 8. Common Testing Patterns & Anti-patterns

### 8.1 Good Patterns

```dart
✅ GOOD: Clear test names
test('displays error message when API returns 403', () {});

✅ GOOD: Arrange-Act-Assert structure
void main() {
  // Arrange
  final stores = StoreFixtures.storeListEntities();

  // Act
  final result = container.read(dashboardProvider);

  // Assert
  expect(result.value, equals(stores));
}

✅ GOOD: Test one thing per test
test('loads stores successfully', () {});
test('handles API error gracefully', () {});

✅ GOOD: Use fixtures for reusable test data
final mockStores = StoreFixtures.storeListEntities();

✅ GOOD: Override providers for isolated testing
final container = ProviderContainer(
  overrides: [
    dashboardProvider.overrideWithValue(const AsyncValue.loading()),
  ],
);

✅ GOOD: Verify interaction counts
verify(() => mockRepository.getAllStores()).called(1);
```

### 8.2 Anti-patterns to Avoid

```dart
❌ BAD: Vague test names
test('test dashboard', () {});

❌ BAD: Testing multiple things
test('loads stores and displays them and handles errors', () {});

❌ BAD: Hardcoded test data scattered across tests
const expected = Store(
  typeNum: 'bk01',
  storeName: 'Test',
  // ... repeated 20 times
);

❌ BAD: Testing implementation instead of behavior
expect(mockRepository.getAllStores, called(1)); // Don't care HOW it works

❌ BAD: Sleeping in tests
await Future.delayed(Duration(seconds: 1));

❌ BAD: Not disposing providers
final container = ProviderContainer();
// No addTearDown(container.dispose);
```

---

## 9. Performance Testing Tips

### 9.1 Benchmark Tests

```dart
// test/performance/provider_build_test.dart
void main() {
  final stopwatch = Stopwatch();

  test('dashboard provider builds in under 100ms', () {
    final container = ProviderContainer();
    addTearDown(container.dispose);

    stopwatch.start();
    container.read(dashboardProvider);
    stopwatch.stop();

    expect(
      stopwatch.elapsedMilliseconds,
      lessThan(100),
      reason: 'Provider took too long to build',
    );
  });
}
```

### 9.2 Memory Leak Detection

```dart
test('provider disposes resources properly', () async {
  var disposed = false;

  final testProvider = FutureProvider((ref) async {
    ref.onDispose(() {
      disposed = true;
    });
    return 'data';
  });

  final container = ProviderContainer();
  container.read(testProvider);

  container.dispose();

  expect(disposed, isTrue);
});
```

---

## 10. Recommended Testing Checklist

### Before Merging PR

- [ ] All unit tests pass (`flutter test`)
- [ ] Coverage remains above 70% for new code
- [ ] Widget tests for new screens/dialogs
- [ ] Error states tested (loading, error, empty)
- [ ] Provider overrides tested (cache invalidation, refresh)
- [ ] Navigation tested for new routes
- [ ] Mocks use `registerFallbackValue` for complex types
- [ ] Fixtures used for reusable test data
- [ ] No `sleep` or hardcoded delays in tests
- [ ] Test names clearly describe behavior
- [ ] Provider disposal verified in tests

### Before Release

- [ ] Integration tests passing for critical flows
- [ ] Coverage report generated and reviewed
- [ ] Performance benchmarks acceptable
- [ ] Manual testing on physical devices
- [ ] Accessibility testing (semantics, contrast)

---

## 11. Additional Resources

### Official Documentation
- [Flutter Testing Overview](https://docs.flutter.dev/testing/overview) - Official Flutter testing docs
- [Riverpod Testing Guide](https://riverpod.dev/docs/essentials/testing) - Riverpod 3.x testing patterns
- [GoRouter Testing](https://guillaume.bernos.dev/testing-go-router/) - Navigation testing approaches

### Community Resources
- [Code with Andrea: AsyncNotifier Testing](https://codewithandrea.com/articles/unit-test-async-notifier-riverpod/) - Detailed AsyncNotifier examples
- [Code with Andrea: Test Coverage](https://codewithandrea.com/articles/flutter-test-coverage/) - Coverage generation and reporting
- [Mocktail Package](https://pub.dev/packages/mocktail) - Zero-config mocking library

### Books & Courses
- "Flutter in Action" by Eric Windmill - Chapter on testing
- "Advanced Flutter" courses covering integration testing

---

## Appendix: Quick Command Reference

```bash
# Test commands
flutter test                              # Run all tests
flutter test --watch                      # Watch mode
flutter test --coverage                   # Generate coverage
flutter test -k "dashboard"               # Run tests matching pattern
flutter test test/domain/                 # Run specific directory
flutter test --fail-fast                  # Stop on first failure
flutter test --concurrency=1              # Run serially (slower but useful for debugging)

# Coverage visualization
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html             # macOS
xdg-open coverage/html/index.html         # Linux

# Code analysis
flutter analyze                           # Check for issues
dart fix --apply                          # Auto-fix issues
dart format .                             # Format code

# Build code generation
dart run build_runner build --delete-conflicting-outputs
dart run build_runner watch               # Regenerate on file changes
```

---

## Summary: Key Takeaways for BuyerKiosk

1. **Testing Pyramid**: Aim for 50-60% unit tests (providers, mappers), 30-35% widget tests (screens), 5-10% integration tests
2. **Riverpod 3.x**: Use `ProviderContainer.test()` with listener-based verification for AsyncNotifier providers
3. **Mocktail**: Preferred over Mockito - simpler setup, no code generation needed
4. **GoRouter**: Test navigation with `MaterialApp.router` and overridden providers
5. **Coverage**: Target 70%+ for production code, use Codecov in CI/CD
6. **Fixtures**: Centralize test data in `test/fixtures/` to keep tests DRY
7. **Dispose**: Always call `addTearDown(container.dispose)` for provider containers
8. **Names**: Write clear test names that describe expected behavior
9. **Performance**: Include benchmark tests for critical code paths
10. **CI/CD**: Automate test execution and coverage reporting with GitHub Actions
