# Flutter Testing Cheat Sheet - 2025

Quick reference for testing patterns in BuyerKiosk Live Flutter app.

## Test Execution

```bash
# Run all tests
flutter test

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

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

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

# Run with coverage
flutter test --coverage

# Run in parallel (faster)
flutter test --concurrency=4

# Run serially (debugging)
flutter test --concurrency=1

# Generate HTML coverage report
flutter test --coverage && genhtml coverage/lcov.info -o coverage/html && open coverage/html/index.html
```

---

## Unit Testing - Riverpod Providers

### Basic AsyncNotifier Test

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mocktail/mocktail.dart';

void main() {
  group('MyProvider', () {
    late MockRepository mockRepo;

    setUpAll(() {
      registerFallbackValue(const AsyncLoading<List<dynamic>>());
      registerFallbackValue(const AsyncData<List<dynamic>>([]));
    });

    setUp(() {
      mockRepo = MockRepository();
    });

    test('loads data successfully', () async {
      // Arrange
      when(() => mockRepo.getData())
          .thenAnswer((_) async => [1, 2, 3]);

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

      final listener = Listener<AsyncValue<List<int>>>();
      container.listen(myProvider, listener);

      // Act & Assert
      await Future<void>.value();
      await container.pump();

      final state = container.read(myProvider);
      expect(state.value, equals([1, 2, 3]));
    });
  });
}
```

### Test Family Provider (with parameter)

```dart
test('loads store detail for specific typeNum', () async {
  const typeNum = 'bk01';

  when(() => mockRepo.getDetail(typeNum))
      .thenAnswer((_) async => StoreDetail(typeNum: typeNum));

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

  final listener = Listener<AsyncValue<StoreDetail>>();
  container.listen(storeDetailProvider(typeNum), listener);

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

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

### Test Provider Refresh

```dart
test('refresh reloads data', () async {
  when(() => mockRepo.getData())
      .thenAnswer((_) async => []);

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

  container.listen(myProvider, (_) {});
  await Future<void>.value();
  await container.pump();

  // Refresh
  await container.read(myProvider.notifier).refresh();

  // Verify called twice
  verify(() => mockRepo.getData()).called(2);
});
```

### Test Error Handling

```dart
test('handles error gracefully', () async {
  when(() => mockRepo.getData())
      .thenThrow(Exception('API Error'));

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

  container.listen(myProvider, (_) {});
  await Future<void>.value();
  await container.pump();

  final state = container.read(myProvider);
  expect(state, isA<AsyncError>());
});
```

---

## Widget Testing

### Basic Widget Test

```dart
testWidgets('displays data correctly', (tester) async {
  await tester.pumpWidget(
    TestApp(
      home: const MyScreen(),
      overrides: [
        myProvider.overrideWithValue(
          const AsyncValue.data(expectedData),
        ),
      ],
    ),
  );

  await tester.pumpAndSettle();

  expect(find.text('Expected Text'), findsOneWidget);
});
```

### Test Loading State

```dart
testWidgets('shows loading indicator', (tester) async {
  await tester.pumpWidget(
    TestApp(
      home: const MyScreen(),
      overrides: [
        myProvider.overrideWithValue(const AsyncValue.loading()),
      ],
    ),
  );

  await tester.pump();
  expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
```

### Test Error State

```dart
testWidgets('displays error message', (tester) async {
  await tester.pumpWidget(
    TestApp(
      home: const MyScreen(),
      overrides: [
        myProvider.overrideWithValue(
          AsyncValue.error(Exception('Error'), StackTrace.current),
        ),
      ],
    ),
  );

  await tester.pumpAndSettle();
  expect(find.text('Error'), findsOneWidget);
});
```

### Test User Interaction

```dart
testWidgets('navigates on tap', (tester) async {
  final mockRouter = MockGoRouter();
  when(() => mockRouter.push(any())).thenAnswer((_) async => null);

  await tester.pumpWidget(
    TestApp(
      home: const MyScreen(),
      overrides: [goRouterProvider.overrideWithValue(mockRouter)],
    ),
  );

  await tester.tap(find.text('Button Text'));
  await tester.pumpAndSettle();

  verify(() => mockRouter.push('/route')).called(1);
});
```

### Test Form Input

```dart
testWidgets('enters text in field', (tester) async {
  await tester.pumpWidget(TestApp(home: const MyScreen()));

  await tester.enterText(find.byType(TextField), 'test input');
  await tester.pumpAndSettle();

  expect(find.text('test input'), findsOneWidget);
});
```

### Test Scrolling

```dart
testWidgets('scrolls to load more items', (tester) async {
  await tester.pumpWidget(TestApp(home: const MyScreen()));
  await tester.pumpAndSettle();

  // Scroll down
  await tester.drag(find.byType(ListView), const Offset(0, -300));
  await tester.pumpAndSettle();

  expect(find.text('Item 10'), findsOneWidget);
});
```

---

## Finding Widgets

```dart
find.byType(MyWidget)                    // By widget type
find.text('Hello')                       // By text content
find.byIcon(Icons.add)                   // By icon
find.byKey(Key('myKey'))                 // By key
find.byTooltip('Tooltip')                // By tooltip
find.bySemanticsLabel('Label')           // By semantics label
find.byWidgetPredicate((w) => ...)       // Custom predicate

// Quantifiers
findsOneWidget                           // Exactly 1
findsWidgets                             // 1 or more
findsNothing                             // 0
findsAtLeastNWidgets(n)                  // n or more
findsNWidgets(n)                         // Exactly n
```

---

## Mocktail Basics

### Setting Up Mocks

```dart
class MockRepository extends Mock {}

final mockRepo = MockRepository();

// Register fallback for type-safe matchers
setUpAll(() {
  registerFallbackValue(const AsyncData<MyType>(null));
});
```

### Stubbing Methods

```dart
// Basic stub
when(() => mockRepo.getData())
    .thenAnswer((_) async => [1, 2, 3]);

// Stub with specific argument
when(() => mockRepo.getById('123'))
    .thenAnswer((_) async => MyData());

// Stub with any argument
when(() => mockRepo.getById(any()))
    .thenAnswer((_) async => MyData());

// Stub to throw
when(() => mockRepo.getData())
    .thenThrow(Exception('Error'));
```

### Verifying Calls

```dart
// Called exactly once
verify(() => mockRepo.getData()).called(1);

// Called at least once
verify(() => mockRepo.getData()).called(greaterThan(0));

// Never called
verifyNever(() => mockRepo.getData());

// Capturing arguments
final captured = <String>[];
when(() => mockRepo.getById(captureAny()))
    .thenAnswer((_) async => MyData());

await mockRepo.getById('123');
expect(captured.first, '123');
```

### Using Matchers with any()

```dart
// Match any value
when(() => mockRepo.getById(any()))
    .thenAnswer((_) async => MyData());

// Match specific type
when(() => mockRepo.getById(any(that: isA<String>())))
    .thenAnswer((_) async => MyData());

// Match with predicate
when(() => mockRepo.getById(any(that: matches(RegExp(r'^\d+$')))))
    .thenAnswer((_) async => MyData());
```

---

## TestApp Helper Widget

```dart
// test/helpers/test_app.dart
class TestApp extends StatelessWidget {
  const TestApp({
    required this.home,
    this.overrides = const [],
    this.router,
  });

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

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

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

---

## Test Fixtures & Test Data

```dart
// test/fixtures/test_data.dart
class TestData {
  static const store = Store(
    typeNum: 'bk01',
    storeName: 'Test Store',
  );

  static final stores = [store, store];

  static final note = WorkbookNote(
    id: 1,
    title: 'Test',
    content: 'Content',
  );
}

// Usage in tests:
when(() => mockRepo.getStore()).thenAnswer((_) async => TestData.store);
```

---

## AsyncValue Testing Patterns

```dart
// AsyncValue states
const AsyncValue.loading()              // Loading
AsyncValue.data(myData)                 // Success
AsyncValue.error(error, stackTrace)     // Error

// Checking state in tests
state.when(
  data: (data) => ...,
  loading: () => ...,
  error: (error, st) => ...,
)

// Reading value
state.value                             // Data value (or null)
state.error                             // Error (or null)
state.isLoading                         // Is loading bool

// When/then matchers
any(that: isA<AsyncLoading>())
any(that: isA<AsyncData<MyType>>())
any(that: isA<AsyncError<MyType>>())
```

---

## Coverage Commands

```bash
# Generate coverage
flutter test --coverage

# View coverage report
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html

# Check minimum coverage (70%)
COVERAGE=$(lcov --summary coverage/lcov.info | grep -oP 'lines\.\.\.: \K[0-9.]+')
if (( $(echo "$COVERAGE < 70" | bc -l) )); then
  echo "Coverage below 70%"
  exit 1
fi
```

---

## GoRouter Navigation Testing

```dart
// Create mock router
final mockRouter = MockGoRouter();

// Stub navigation
when(() => mockRouter.push('/route')).thenAnswer((_) async => null);
when(() => mockRouter.go('/route')).thenAnswer((_) async => null);

// Override in test
overrides: [
  goRouterProvider.overrideWithValue(mockRouter),
]

// Verify navigation
verify(() => mockRouter.push('/store/bk01')).called(1);
```

---

## Common Test Patterns

### Arrange-Act-Assert

```dart
test('does something', () {
  // ARRANGE: Setup
  final mockRepo = MockRepository();
  when(() => mockRepo.getData()).thenAnswer((_) async => []);

  // ACT: Execute
  final result = await myFunction(mockRepo);

  // ASSERT: Verify
  expect(result, isNotEmpty);
});
```

### Listener Pattern (for AsyncValue)

```dart
test('emits states in order', () async {
  final listener = Listener<AsyncValue<MyType>>();
  container.listen(myProvider, listener);

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

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

  verify(() => listener(
    any(that: isA<AsyncLoading>()),
    any(that: isA<AsyncData<MyType>>()),
  )).called(1);
});
```

### Provider Override

```dart
final container = ProviderContainer(
  overrides: [
    repositoryProvider.overrideWithValue(mockRepo),
    anotherProvider.overrideWithValue(mockValue),
  ],
);
addTearDown(container.dispose);
```

---

## Debugging Tests

```bash
# Run test with output
flutter test test/my_test.dart -v

# Run test and show all print statements
flutter test test/my_test.dart -v --verbose-logging

# Stop on first failure
flutter test --fail-fast

# Run test and keep running
flutter test --watch

# Run single test
flutter test test/my_test.dart -k "specific test name"
```

---

## What NOT to Do

```dart
// DON'T: Sleep in tests
❌ await Future.delayed(Duration(seconds: 1));

// DO: Use pumpAndSettle
✅ await tester.pumpAndSettle();

// DON'T: Hardcode test data everywhere
❌ const store = Store(typeNum: 'test', storeName: 'Test');

// DO: Use fixtures
✅ final store = StoreFixtures.testStore;

// DON'T: Test multiple things in one test
❌ test('loads, displays, and navigates', () {});

// DO: One assertion per test
✅ test('displays loaded data', () {});

// DON'T: Forget to dispose providers
❌ final container = ProviderContainer();

// DO: Add teardown
✅ final container = ProviderContainer();
✅ addTearDown(container.dispose);

// DON'T: Compare AsyncLoading directly
❌ expect(state, equals(AsyncLoading()));

// DO: Use type matchers
✅ expect(state, isA<AsyncLoading>());
```

---

## Resources

- [Flutter Testing Docs](https://docs.flutter.dev/testing/overview)
- [Riverpod Testing Guide](https://riverpod.dev/docs/essentials/testing)
- [Mocktail Package](https://pub.dev/packages/mocktail)
- [GoRouter Testing](https://guillaume.bernos.dev/testing-go-router/)
- [Full TESTING_GUIDE.md](./TESTING_GUIDE.md) - Complete guide with examples

---

## Quick Checklist Before Merging

- [ ] `flutter test` passes
- [ ] Coverage above 70% for new code
- [ ] New screens have widget tests
- [ ] Error states tested (loading, error, empty)
- [ ] Navigation tested with mocks
- [ ] No `sleep()` or hardcoded delays
- [ ] Fixtures used for reusable test data
- [ ] Providers disposed with `addTearDown()`
- [ ] Test names describe behavior
- [ ] `flutter analyze` passes
