
Flutter Tester
- 312 installs
- 58 repo stars
- Updated July 12, 2026
- harishwarrior/flutter-claude-skills
Writes and reviews Flutter unit, widget, and integration tests with Riverpod, Mockito, and GetIt using layer-isolation and Given-When-Then patterns.
About
A Flutter testing skill that tests each architectural layer against its own mocked dependencies and covers both success and error paths. A developer uses it to structure repository, DAO, provider, service, and widget tests.
- Layer-isolation table mapping what to test vs what to mock per layer
- Overrides Riverpod provider dependencies instead of mocking providers
Flutter Tester by the numbers
- 312 all-time installs (skills.sh)
- +18 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #696 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/harishwarrior/flutter-claude-skills --skill flutter-testerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 312 |
|---|---|
| repo stars | ★ 58 |
| Last updated | July 12, 2026 |
| Repository | harishwarrior/flutter-claude-skills ↗ |
What it does
Writes and reviews Flutter unit, widget, and integration tests with Riverpod, Mockito, and GetIt using layer-isolation and Given-When-Then patterns.
Files
Flutter Tester
Requirements
- Flutter project with
flutter_testdependency - Works with Riverpod, Mockito, and GetIt
- Run
dart run build_runner buildto generate mocks after adding@GenerateMocksannotations - Compatible with FVM (
fvm flutter testinstead offlutter 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 resetFakes vs Mocks
- Fakes (
class FakeLogger extends ILogger) — silent stubs; use when you don't need to verify calls - Mocks (
MockILogger) — use when you needwhen(),verify(), orthenThrow()
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 ManagerCommon 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()intearDown - [ ] 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()orverifyNever()where appropriate - [ ] Tests are isolated and deterministic
Layer Testing Patterns
This file provides comprehensive examples for testing each architectural layer in Flutter applications.
Repository Layer Testing
Repositories coordinate between DAOs and APIs. Test both success and error scenarios.
Basic Repository Test Pattern
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
// Generate mocks for dependencies
@GenerateMocks([UserDAO, UserAPI, Logger])
void main() {
late UserRepository repository;
late MockUserDAO mockDAO;
late MockUserAPI mockAPI;
late MockLogger mockLogger;
setUp(() {
mockDAO = MockUserDAO();
mockAPI = MockUserAPI();
mockLogger = MockLogger();
repository = UserRepository(
dao: mockDAO,
api: mockAPI,
logger: mockLogger,
);
});
group('UserRepository', () {
group('fetchUsers', () {
test('Given successful DAO call, when fetchUsers is called, then returns list', () async {
// Arrange
final expectedUsers = [
User(id: '1', name: 'John'),
User(id: '2', name: 'Jane'),
];
when(mockDAO.fetchAll()).thenAnswer((_) async => expectedUsers);
// Act
final result = await repository.fetchUsers();
// Assert
expect(result, equals(expectedUsers));
verify(mockDAO.fetchAll()).called(1);
});
test('Given DAO throws exception, when fetchUsers is called, then returns empty and logs error', () async {
// Arrange
final exception = Exception('Database error');
when(mockDAO.fetchAll()).thenThrow(exception);
// Act
final result = await repository.fetchUsers();
// Assert
expect(result, isEmpty);
verify(mockDAO.fetchAll()).called(1);
verify(mockLogger.logException('UserRepository', 'fetchUsers', exception, any)).called(1);
});
});
group('syncWithAPI', () {
test('Given successful API sync, when syncWithAPI is called, then updates database', () async {
// Arrange
final apiData = [User(id: '1', name: 'John Updated')];
when(mockAPI.fetchUsers()).thenAnswer((_) async => apiData);
when(mockDAO.insertAll(any)).thenAnswer((_) async => 1);
// Act
await repository.syncWithAPI();
// Assert
verify(mockAPI.fetchUsers()).called(1);
verify(mockDAO.insertAll(apiData)).called(1);
});
test('Given API throws exception, when syncWithAPI is called, then logs error and does not update DB', () async {
// Arrange
final exception = Exception('Network error');
when(mockAPI.fetchUsers()).thenThrow(exception);
// Act
await repository.syncWithAPI();
// Assert
verify(mockAPI.fetchUsers()).called(1);
verifyNever(mockDAO.insertAll(any));
verify(mockLogger.logException('UserRepository', 'syncWithAPI', exception, any)).called(1);
});
});
});
}Repository Testing Key Patterns
- Mock both data sources (DAO and API)
- Test success paths - Verify data flows correctly
- Test error paths - Verify exceptions are handled and logged
- Verify method calls - Use
verify()to ensure dependencies are called correctly - Use `verifyNever()` - Ensure methods aren't called when they shouldn't be
DAO Layer Testing
DAOs interact with the database. Use FakeDatabase for realistic testing.
Basic DAO Test Pattern
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:sqflite/sqflite.dart';
@GenerateMocks([Logger])
void main() {
late ItemDAO itemDAO;
late Database db;
late MockLogger mockLogger;
setUp(() async {
// Setup fake database (implementation depends on your project)
db = await openInMemoryDatabase();
mockLogger = MockLogger();
itemDAO = ItemDAO(
database: db,
logger: mockLogger,
);
// Create tables
await itemDAO.createTable();
});
tearDown(() async {
await itemDAO.deleteTable();
await db.close();
});
group('ItemDAO', () {
group('insert', () {
test('Given valid item, when insert is called, then item is added to database', () async {
// Arrange
final item = Item(id: '1', name: 'Test Item');
// Act
final result = await itemDAO.insert(item);
// Assert
expect(result, greaterThan(0)); // Row ID returned
final fetchedItems = await itemDAO.fetchAll();
expect(fetchedItems, hasLength(1));
expect(fetchedItems.first.name, 'Test Item');
});
test('Given insert fails, when insert is called, then logs exception', () async {
// Arrange
final invalidItem = Item(id: null, name: null); // Invalid data
// Act & Assert
expect(() => itemDAO.insert(invalidItem), throwsException);
});
});
group('fetchAll', () {
test('Given items in database, when fetchAll is called, then returns all items', () async {
// Arrange
await itemDAO.insert(Item(id: '1', name: 'Item 1'));
await itemDAO.insert(Item(id: '2', name: 'Item 2'));
// Act
final result = await itemDAO.fetchAll();
// Assert
expect(result, hasLength(2));
expect(result.map((i) => i.name), containsAll(['Item 1', 'Item 2']));
});
test('Given empty database, when fetchAll is called, then returns empty list', () async {
// Act
final result = await itemDAO.fetchAll();
// Assert
expect(result, isEmpty);
});
});
group('update', () {
test('Given existing item, when update is called, then item is updated', () async {
// Arrange
await itemDAO.insert(Item(id: '1', name: 'Original'));
final updatedItem = Item(id: '1', name: 'Updated');
// Act
await itemDAO.update(updatedItem);
// Assert
final result = await itemDAO.fetchAll();
expect(result.first.name, 'Updated');
});
});
group('delete', () {
test('Given existing item, when delete is called, then item is removed', () async {
// Arrange
await itemDAO.insert(Item(id: '1', name: 'To Delete'));
// Act
await itemDAO.delete('1');
// Assert
final result = await itemDAO.fetchAll();
expect(result, isEmpty);
});
});
});
}DAO Testing Key Patterns
- Use in-memory database - Fast and isolated tests
- Create/delete tables in setUp/tearDown
- Test CRUD operations - Create, Read, Update, Delete
- Test empty states - Verify behavior with no data
- Test data integrity - Ensure inserts/updates work correctly
Provider Layer Testing (Riverpod)
Providers manage state. Test state transitions and async flows.
Basic Provider Test Pattern
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@GenerateMocks([DataService, Logger])
void main() {
late MockDataService mockService;
late MockLogger mockLogger;
setUp(() {
mockService = MockDataService();
mockLogger = MockLogger();
});
group('DataProvider', () {
test('Given service returns data, when provider builds, then state contains data', () async {
// Arrange
final expectedData = [
DataModel(id: '1', name: 'Item 1'),
DataModel(id: '2', name: 'Item 2'),
];
when(mockService.fetchData()).thenAnswer((_) async => expectedData);
// Create container with overrides
final container = ProviderContainer(
overrides: [
dataServiceProvider.overrideWithValue(mockService),
loggerProvider.overrideWithValue(mockLogger),
],
);
addTearDown(container.dispose);
// Act
final state = await container.read(dataProvider.future);
// Assert
expect(state, equals(expectedData));
verify(mockService.fetchData()).called(1);
});
test('Given service throws exception, when provider builds, then error state is set', () async {
// Arrange
final exception = Exception('Network error');
when(mockService.fetchData()).thenThrow(exception);
final container = ProviderContainer(
overrides: [
dataServiceProvider.overrideWithValue(mockService),
],
);
addTearDown(container.dispose);
// Act & Assert
expect(
() => container.read(dataProvider.future),
throwsA(exception),
);
});
test('Given state changes, when updateData is called, then new state is emitted', () async {
// Arrange
when(mockService.fetchData()).thenAnswer((_) async => []);
final container = ProviderContainer(
overrides: [
dataServiceProvider.overrideWithValue(mockService),
],
);
addTearDown(container.dispose);
final notifier = container.read(dataProvider.notifier);
final newData = [DataModel(id: '3', name: 'New Item')];
// Act
notifier.updateData(newData);
// Assert
final state = container.read(dataProvider);
expect(state.value, equals(newData));
});
});
}Provider Testing Key Patterns
- Override providers - Use
ProviderContainerwith overrides - Test async states - Handle loading, data, and error states
- Dispose containers - Use
addTearDown(container.dispose) - Test state changes - Verify notifier methods update state correctly
- Mock service dependencies - Never mock providers directly
Service Layer Testing
Services contain business logic. Test the logic, not the implementation.
Basic Service Test Pattern
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
@GenerateMocks([Repository, Logger, NetworkClient])
void main() {
late DataService service;
late MockRepository mockRepository;
late MockLogger mockLogger;
late MockNetworkClient mockClient;
setUp(() {
mockRepository = MockRepository();
mockLogger = MockLogger();
mockClient = MockNetworkClient();
service = DataService(
repository: mockRepository,
logger: mockLogger,
client: mockClient,
);
});
group('DataService', () {
test('Given cached data exists, when getData is called, then returns cached data without API call', () async {
// Arrange
final cachedData = [DataModel(id: '1', name: 'Cached')];
when(mockRepository.getCached()).thenAnswer((_) async => cachedData);
// Act
final result = await service.getData(useCache: true);
// Assert
expect(result, equals(cachedData));
verify(mockRepository.getCached()).called(1);
verifyNever(mockClient.fetch(any));
});
test('Given no cached data, when getData is called, then fetches from API and caches', () async {
// Arrange
final apiData = [DataModel(id: '2', name: 'From API')];
when(mockRepository.getCached()).thenAnswer((_) async => null);
when(mockClient.fetch(any)).thenAnswer((_) async => apiData);
when(mockRepository.save(any)).thenAnswer((_) async => {});
// Act
final result = await service.getData(useCache: true);
// Assert
expect(result, equals(apiData));
verify(mockRepository.getCached()).called(1);
verify(mockClient.fetch(any)).called(1);
verify(mockRepository.save(apiData)).called(1);
});
test('Given API call fails, when getData is called, then returns cached data as fallback', () async {
// Arrange
final cachedData = [DataModel(id: '1', name: 'Cached')];
when(mockRepository.getCached()).thenAnswer((_) async => cachedData);
when(mockClient.fetch(any)).thenThrow(Exception('Network error'));
// Act
final result = await service.getData(useCache: false, fallbackToCache: true);
// Assert
expect(result, equals(cachedData));
verify(mockClient.fetch(any)).called(1);
verify(mockRepository.getCached()).called(1);
});
});
}Service Testing Key Patterns
- Test business logic - Focus on the "what", not the "how"
- Test caching strategies - Verify cache hits/misses
- Test fallback behavior - Ensure graceful degradation
- Mock all dependencies - Services should have no direct I/O
Summary
Testing Each Layer
| 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 |
Common Verification Patterns
// Verify method was called once
verify(mock.method()).called(1);
// Verify method was called with specific arguments
verify(mock.method(argThat(equals('expected')))).called(1);
// Verify method was never called
verifyNever(mock.method());
// Verify call order
verifyInOrder([
mock.method1(),
mock.method2(),
]);
// Verify no more interactions
verifyNoMoreInteractions(mock);Error Handling Patterns
// Test that method throws
expect(() => service.method(), throwsException);
// Test specific exception
expect(() => service.method(), throwsA(isA<CustomException>()));
// Test async throws
expect(() async => await service.method(), throwsA(isA<NetworkException>()));
// Verify logging on error
verify(mockLogger.logException(any, any, exception, any)).called(1);Riverpod Testing Guide
This file provides advanced patterns for testing Riverpod providers in the Flutter application.
Core Principle: Never Mock Providers
❌ DON'T mock providers:
// BAD - Don't do this!
@GenerateMocks([NotificationNotifierProvider])✅ DO override provider dependencies:
final container = createContainer(overrides: [
notificationServiceProvider.overrideWith((ref) => mockNotificationService),
]);Container Setup
createContainer Helper
Copy this helper into your test support files (e.g. test/helpers/riverpod_container.dart). The deep relative path in the import must match your project structure — there is no universal path.
ProviderContainer createContainer({
ProviderContainer? parent,
List<Override> overrides = const [],
List<ProviderObserver>? observers,
}) {
final container = ProviderContainer(
parent: parent,
overrides: overrides,
observers: observers,
);
addTearDown(container.dispose);
return container;
}Use it in tests:
final container = createContainer(overrides: [
serviceProvider.overrideWith((ref) => mockService),
]);
// Container automatically disposed in addTearDownWarning — do NOT call `container.dispose()` manually when you obtained the container fromcreateContainer(). The helper already registers anaddTearDownthat disposes it. A second explicit dispose causes aStateError. If you need to testref.onDisposebehaviour, create the container manually instead (see "Testing Provider Lifecycle" below).
Testing AsyncNotifierProvider
Testing Initial State (build method)
test('Given empty notifications and default audio config, '
'when building initial state, '
'then should return default NotificationState', () async {
// Arrange
when(mockNotificationService.fetchNotifications()).thenAnswer((_) async => []);
when(mockNotificationService.getAudioConfiguration()).thenAnswer(
(_) async => (isAudioMuted: false, isAudioEnabled: true),
);
final container = createContainer();
// Act
final notifier = container.read(notificationNotifierProvider.notifier);
final state = await notifier.future;
// Assert
expect(state.notifications, isEmpty);
expect(state.isAudioEnabled, true);
expect(state.isAudioMuted, false);
expect(state.shouldPlayAudio, false);
expect(state.audioFileNames, isEmpty);
expect(state.userList, isEmpty);
expect(state.audioFileIndex, 0);
expect(state.wasRadioPlaying, false);
verify(mockNotificationService.fetchNotifications()).called(1);
verify(mockNotificationService.getAudioConfiguration()).called(1);
});Testing State with Data
test('Given notifications with audio enabled and not muted, '
'when building initial state, '
'then should process audio and return populated state', () async {
// Arrange
const testNotifications = [
NotificationData(
idleTimeOut: 300,
appType: 1,
audioStatus: 1,
url: 'https://test.com',
refName: 'TestRef',
menuName: 'TestMenu',
userId: 'user1',
userName: 'Test User',
),
];
when(mockNotificationService.fetchNotifications()).thenAnswer((_) async => testNotifications);
when(mockNotificationService.getAudioConfiguration()).thenAnswer(
(_) async => (isAudioMuted: false, isAudioEnabled: true),
);
when(
mockNotificationService.processAudioForNotifications(
notificationList: testNotifications,
isAudioEnabled: true,
shouldPlayAudio: true,
wasRadioPlaying: false,
currentPlayer: null,
isAudioMuted: false,
),
).thenAnswer(
(_) async => (
audioFiles: ['TestRef'],
fileIndex: 0,
wasRadioPlaying: false,
player: mockAudioPlayer,
),
);
final container = createContainer();
// Act
final notifier = container.read(notificationNotifierProvider.notifier);
final state = await notifier.future;
// Assert
expect(state.notifications, testNotifications);
expect(state.isAudioEnabled, true);
expect(state.isAudioMuted, false);
expect(state.shouldPlayAudio, true);
expect(state.audioFileNames, ['TestRef']);
expect(state.userList, ['user1']);
expect(state.audioFileIndex, 0);
expect(state.wasRadioPlaying, false);
expect(state.audioPlayer, mockAudioPlayer);
});Testing State Mutations
Testing Simple State Updates
test('Given valid state, '
'when setShouldPlayAudio is called with true, '
'then should update shouldPlayAudio to true', () async {
// Arrange
when(mockNotificationService.fetchNotifications()).thenAnswer((_) async => []);
when(mockNotificationService.getAudioConfiguration()).thenAnswer(
(_) async => (isAudioMuted: false, isAudioEnabled: true),
);
final container = createContainer();
final notifier = container.read(notificationNotifierProvider.notifier);
await notifier.future;
// Act
notifier.setShouldPlayAudio(value: true);
// Assert
final state = container.read(notificationNotifierProvider).value!;
expect(state.shouldPlayAudio, true);
});Testing Async State Updates (refreshState)
test('Given valid service responses, '
'when refreshState is called, '
'then should update state with fresh data', () async {
// Arrange
const initialNotifications = [
NotificationData(refName: 'Initial', userId: 'user1'),
];
const updatedNotifications = [
NotificationData(refName: 'Updated', userId: 'user2'),
];
when(mockNotificationService.fetchNotifications()).thenAnswer((_) async => initialNotifications);
when(mockNotificationService.getAudioConfiguration()).thenAnswer(
(_) async => (isAudioMuted: false, isAudioEnabled: true),
);
final container = createContainer();
final notifier = container.read(notificationNotifierProvider.notifier);
await notifier.future;
// Update mock for refreshState
when(mockNotificationService.fetchNotifications()).thenAnswer((_) async => updatedNotifications);
when(
mockNotificationService.processAudioForNotifications(
notificationList: updatedNotifications,
isAudioEnabled: true,
shouldPlayAudio: true,
wasRadioPlaying: false,
currentPlayer: null,
isAudioMuted: false,
),
).thenAnswer(
(_) async => (
audioFiles: ['Updated'],
fileIndex: 0,
wasRadioPlaying: false,
player: mockAudioPlayer,
),
);
// Act
await notifier.refreshState();
// Assert
final state = container.read(notificationNotifierProvider).value!;
expect(state.notifications, updatedNotifications);
expect(state.userList, ['user2']);
expect(state.audioFileNames, ['Updated']);
verify(mockNotificationService.fetchNotifications()).called(2); // Initial + refresh
});Testing Error Handling
Testing AsyncError State
test('Given service throws exception, '
'when refreshState is called, '
'then should set state to AsyncError', () async {
// Arrange
when(mockNotificationService.fetchNotifications()).thenAnswer((_) async => []);
when(mockNotificationService.getAudioConfiguration()).thenAnswer(
(_) async => (isAudioMuted: false, isAudioEnabled: true),
);
final container = createContainer();
final notifier = container.read(notificationNotifierProvider.notifier);
await notifier.future;
// Setup exception for refreshState
when(mockNotificationService.fetchNotifications()).thenThrow(Exception('Network error'));
// Act
await notifier.refreshState();
// Assert
final asyncState = container.read(notificationNotifierProvider);
expect(asyncState.hasError, true);
expect(asyncState.error.toString(), contains('Network error'));
});Testing Error Propagation
test('Given audio processing throws exception, '
'when building initial state with audio enabled, '
'then should propagate error', () async {
// Arrange
const testNotifications = [
NotificationData(audioStatus: 1, refName: 'TestRef'),
];
when(mockNotificationService.fetchNotifications()).thenAnswer((_) async => testNotifications);
when(mockNotificationService.getAudioConfiguration()).thenAnswer(
(_) async => (isAudioMuted: false, isAudioEnabled: true),
);
when(
mockNotificationService.processAudioForNotifications(
notificationList: anyNamed('notificationList'),
isAudioEnabled: anyNamed('isAudioEnabled'),
shouldPlayAudio: anyNamed('shouldPlayAudio'),
wasRadioPlaying: anyNamed('wasRadioPlaying'),
currentPlayer: anyNamed('currentPlayer'),
isAudioMuted: anyNamed('isAudioMuted'),
),
).thenThrow(Exception('Audio processing failed'));
final container = createContainer();
// Act & Assert
expect(() async {
final notifier = container.read(notificationNotifierProvider.notifier);
await notifier.future;
}, throwsA(isA<Exception>()));
});Testing Provider Dependencies
Overriding Service Dependencies
final container = createContainer(overrides: [
// Override the service provider with a mock
notificationServiceProvider.overrideWith((ref) => mockNotificationService),
// Override repository provider
notificationRepositoryProvider.overrideWith((ref) => mockRepository),
// Can override multiple providers
loggerProvider.overrideWith((ref) => mockLogger),
]);Testing Provider Reads
test('Given provider dependencies, '
'when provider is built, '
'then should read dependencies correctly', () async {
// Arrange
when(mockNotificationService.fetchNotifications()).thenAnswer((_) async => []);
when(mockNotificationService.getAudioConfiguration()).thenAnswer(
(_) async => (isAudioMuted: false, isAudioEnabled: true),
);
final container = createContainer(overrides: [
notificationServiceProvider.overrideWith((ref) => mockNotificationService),
]);
// Act
final notifier = container.read(notificationNotifierProvider.notifier);
await notifier.future;
// Assert
verify(mockNotificationService.fetchNotifications()).called(1);
verify(mockNotificationService.getAudioConfiguration()).called(1);
});Testing Provider Lifecycle
Testing ref.onDispose
When testing ref.onDispose, you need to call container.dispose() yourself to trigger it. Do not use `createContainer()` here — it already registers addTearDown(container.dispose), which would dispose the container a second time and throw a StateError. Create the container manually instead:
test('Given provider is disposed, '
'when container is disposed, '
'then should unsubscribe from notifications and dispose audio', () async {
// Arrange
when(mockNotificationService.fetchNotifications()).thenAnswer((_) async => []);
when(mockNotificationService.getAudioConfiguration()).thenAnswer(
(_) async => (isAudioMuted: false, isAudioEnabled: true),
);
when(mockNotificationService.disposeAudioPlayer(any)).thenAnswer((_) async {});
// Use ProviderContainer directly — NOT createContainer() — to avoid double-dispose.
// createContainer() already registers addTearDown(container.dispose); calling
// dispose() manually on top of that causes a StateError.
final container = ProviderContainer(overrides: [
notificationServiceProvider.overrideWith((ref) => mockNotificationService),
]);
final notifier = container.read(notificationNotifierProvider.notifier);
await notifier.future;
// Act - Dispose the container to trigger ref.onDispose
container.dispose();
// Assert - Verify unsubscribe calls
verify(
mockNotificationCenter.unsubscribe(
channel: AppConstants.notificationAudioChannel,
observer: notifier,
),
).called(1);
verify(
mockNotificationCenter.unsubscribe(
channel: AppConstants.notificationUpdateChannel,
observer: notifier,
),
).called(1);
});Testing Complex State Transitions
Testing State with Conditional Logic
Avoid direct `notifier.state = ...` assignment. It bypasses the notifier's public API and tightly couples tests toAsyncNotifierinternals — your test breaks whenever the internal state shape changes. Prefer calling a public method that sets the state you need (e.g.notifier.setAudioPlayer(mockPlayer)). If no such method exists, consider adding one or restructuring the test to reach the desired state through the normal flow.
When no public setter exists and direct assignment is the only option, document it clearly:
test('Given audio config changes to muted during refresh, '
'when refreshState is called, '
'then should disable audio and keep existing player', () async {
// Arrange
const testNotifications = [
NotificationData(refName: 'TestRef', userId: 'user1', audioStatus: 1),
];
when(mockNotificationService.fetchNotifications()).thenAnswer((_) async => testNotifications);
when(mockNotificationService.getAudioConfiguration()).thenAnswer(
(_) async => (isAudioMuted: false, isAudioEnabled: true),
);
when(mockNotificationService.disposeAudioPlayer(any)).thenAnswer((_) async {});
final container = createContainer();
final notifier = container.read(notificationNotifierProvider.notifier);
await notifier.future;
// Direct state assignment used here because no public setter exists.
// Prefer a public method if one becomes available.
notifier.state = AsyncData(
notifier.state.value!.copyWith(audioPlayer: mockAudioPlayer),
);
// Change audio config to muted for refresh
when(mockNotificationService.getAudioConfiguration()).thenAnswer(
(_) async => (isAudioMuted: true, isAudioEnabled: true),
);
// Act
await notifier.refreshState();
// Assert
final state = container.read(notificationNotifierProvider).value!;
expect(state.isAudioMuted, true);
expect(state.shouldPlayAudio, false);
expect(state.audioFileNames, isEmpty);
});Testing State with Resource Cleanup
test('Given existing audio player and new notifications with audio, '
'when refreshState is called, '
'then should dispose old player and create new one', () async {
// Arrange
const initialNotifications = [
NotificationData(refName: 'Initial', userId: 'user1', audioStatus: 1),
];
const updatedNotifications = [
NotificationData(refName: 'Updated1', userId: 'user2', audioStatus: 1),
NotificationData(refName: 'Updated2', userId: 'user3', audioStatus: 1),
];
final mockAudioPlayer2 = MockAudioPlayer();
when(mockNotificationService.fetchNotifications()).thenAnswer((_) async => initialNotifications);
when(mockNotificationService.getAudioConfiguration()).thenAnswer(
(_) async => (isAudioMuted: false, isAudioEnabled: true),
);
when(mockNotificationService.disposeAudioPlayer(any)).thenAnswer((_) async {});
final container = createContainer();
final notifier = container.read(notificationNotifierProvider.notifier);
await notifier.future;
// Direct state assignment used to pre-load audio player and radio state.
// Replace with a public setter (e.g. notifier.setAudioPlayer(...)) if one exists.
notifier.state = AsyncData(
notifier.state.value!.copyWith(
audioPlayer: mockAudioPlayer,
wasRadioPlaying: true,
),
);
// Update for refreshState with new notifications
when(mockNotificationService.fetchNotifications()).thenAnswer((_) async => updatedNotifications);
when(
mockNotificationService.processAudioForNotifications(
notificationList: updatedNotifications,
isAudioEnabled: true,
shouldPlayAudio: true,
wasRadioPlaying: true,
currentPlayer: null,
isAudioMuted: false,
),
).thenAnswer(
(_) async => (
audioFiles: ['Updated1', 'Updated2'],
fileIndex: 1,
wasRadioPlaying: false,
player: mockAudioPlayer2,
),
);
// Act
await notifier.refreshState();
// Assert
final state = container.read(notificationNotifierProvider).value!;
expect(state.notifications, updatedNotifications);
expect(state.audioFileNames, ['Updated1', 'Updated2']);
expect(state.audioFileIndex, 1);
expect(state.wasRadioPlaying, false);
expect(state.audioPlayer, mockAudioPlayer2);
expect(state.userList, ['user2', 'user3']);
// Verify the existing player was disposed
verify(mockNotificationService.disposeAudioPlayer(mockAudioPlayer)).called(1);
});Testing Notification Subscriptions
Testing Channel Subscriptions
Subscription callbacks cannot be meaningfully unit-tested by simulating them manually. Directly calling the service and setting notifier.state yourself tests nothing about whether the subscription is wired up — it only tests the state shape. True subscription testing requires either:>
1. Extract the callback to a public method (e.g. notifier.onAudioConfigChanged(config)) and unit-test that method directly.2. Integration test the full subscription flow end-to-end.
>
The example below tests the extracted method approach — the recommended pattern:
test('Given audio config changes to muted, '
'when onAudioConfigChanged is called, '
'then should update isAudioMuted and isAudioEnabled in state', () async {
// Arrange
when(mockNotificationService.fetchNotifications()).thenAnswer((_) async => []);
when(mockNotificationService.getAudioConfiguration()).thenAnswer(
(_) async => (isAudioMuted: false, isAudioEnabled: true),
);
final container = createContainer();
final notifier = container.read(notificationNotifierProvider.notifier);
await notifier.future;
// Act - Call the public method that the subscription callback delegates to
await notifier.onAudioConfigChanged(isAudioMuted: true, isAudioEnabled: false);
// Assert
final state = container.read(notificationNotifierProvider).value!;
expect(state.isAudioMuted, true);
expect(state.isAudioEnabled, false);
});Testing Edge Cases
Testing Empty or Null Data
test('Given empty userId values in notifications, '
'when building state, '
'then should include empty strings in userList', () async {
// Arrange
const testNotifications = [
NotificationData(userId: 'user1', userName: 'User One'),
NotificationData(userName: 'Empty User'), // Empty userId
NotificationData(userName: 'No ID User'),
NotificationData(userId: 'user2', userName: 'User Two'),
];
when(mockNotificationService.fetchNotifications()).thenAnswer((_) async => testNotifications);
when(mockNotificationService.getAudioConfiguration()).thenAnswer(
(_) async => (isAudioMuted: true, isAudioEnabled: true),
);
final container = createContainer();
// Act
final notifier = container.read(notificationNotifierProvider.notifier);
final state = await notifier.future;
// Assert
expect(state.userList, ['user1', '', 'user2']);
expect(state.userList.length, 3);
});Testing State Consistency
test('Given multiple notifications with different users, '
'when building initial state, '
'then should extract unique user list', () async {
// Arrange
const testNotifications = [
NotificationData(userId: 'user1', userName: 'User One'),
NotificationData(userId: 'user2', userName: 'User Two'),
NotificationData(userId: 'user1', userName: 'User One'), // Duplicate
NotificationData(userId: 'user3', userName: 'User Three'),
];
when(mockNotificationService.fetchNotifications()).thenAnswer((_) async => testNotifications);
when(mockNotificationService.getAudioConfiguration()).thenAnswer(
(_) async => (isAudioMuted: true, isAudioEnabled: true),
);
final container = createContainer();
// Act
final notifier = container.read(notificationNotifierProvider.notifier);
final state = await notifier.future;
// Assert
expect(state.userList, containsAll(['user1', 'user2', 'user3']));
expect(state.userList.length, 3); // Should be unique
});Riverpod Testing Patterns Summary
Key Principles
1. Never mock providers - override dependencies 2. Use createContainer() with overrides 3. Test initial state (build method) 4. Test state mutations 5. Test async state updates 6. Test error handling (AsyncError) 7. Test lifecycle (ref.onDispose) 8. Test subscriptions and notifications 9. Verify service method calls 10. Test edge cases and null handling
Common Setup Pattern
late MockINotificationService mockNotificationService;
late MockILogger mockLogger;
setUp(() {
mockNotificationService = MockINotificationService();
mockLogger = MockILogger();
GetIt.I.reset();
GetIt.I
..registerSingleton<INotificationService>(mockNotificationService)
..registerSingleton<ILogger>(mockLogger);
});
tearDown(() {
GetIt.I.reset();
});Common Test Pattern
test('Given [condition], when [action], then [expected result]', () async {
// Arrange
when(mockService.method()).thenAnswer((_) async => mockData);
final container = createContainer();
final notifier = container.read(provider.notifier);
await notifier.future;
// Act
await notifier.performAction();
// Assert
final state = container.read(provider).value!;
expect(state.property, expectedValue);
verify(mockService.method()).called(1);
});Widget Testing Guide
This file provides comprehensive patterns for testing Flutter widgets in the Flutter application.
Essential Widget Test Setup
Always Set Screen Size
Prevent overflow errors by setting explicit screen dimensions. Prefer hoisting this into setUp so it applies to every test in the group and resets automatically — this avoids repeating the two lines in every testWidgets block:
// ✅ PREFERRED — set once per group, auto-reset
setUp(() {
tester.view.physicalSize = const Size(1000, 1000);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
});If you only need it for a single test, set it inline:
testWidgets('Test description', (tester) async {
tester.view.physicalSize = const Size(1000, 1000);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
// Your test code
});Create Test Widget Wrapper
Widget createTestWidget() => ProviderScope(
child: MaterialApp(home: NotificationView()),
);
// Or with MockGoRouterProvider
Widget createTestWidget() => ProviderScope(
child: MockGoRouterProvider(
goRouter: fakeGoRouter,
child: const MaterialApp(home: RegistrationView()),
),
);Finding Widgets with Keys
Why Use Keys
Keys provide stable, reliable widget identification that won't break when:
- Text content changes
- Widget positions shift
- Multiple similar widgets exist
- Widget tree reorganizes
Adding Keys to Source Widgets
// ❌ BAD - No key
ElevatedButton(
onPressed: () {},
child: const Text('Save'),
);
// ✅ GOOD - Has key
ElevatedButton(
key: const Key('saveButton'),
onPressed: () {},
child: const Text('Save'),
);Using Keys in Tests
testWidgets('Given save button, When tapped, Then calls save action', (tester) async {
tester.view.physicalSize = const Size(1000, 1000);
tester.view.devicePixelRatio = 1.0;
await tester.pumpWidget(createTestWidget());
// Find by key
await tester.tap(find.byKey(const Key('saveButton')));
await tester.pumpAndSettle();
// Verify action
verify(mockService.saveData()).called(1);
});Common Key Naming Conventions
// Buttons
key: const Key('saveButton')
key: const Key('cancelButton')
key: const Key('submitButton')
// Input fields
key: const Key('emailTextField')
key: const Key('passwordTextField')
// Containers/Sections
key: const Key('headerSection')
key: const Key('footerSection')
// List items
key: Key('listItem_$index')
key: Key('notification_${notification.id}')User Interaction Patterns
Tapping Widgets
// Tap by key
await tester.tap(find.byKey(const Key('saveButton')));
await tester.pump();
// Tap by text (less stable)
await tester.tap(find.text('Save'));
await tester.pump();
// Tap by type (when only one widget of this type exists)
await tester.tap(find.byType(ElevatedButton));
await tester.pump();Text Input
// Enter text into first TextField
await tester.enterText(find.byType(TextField).first, 'example.com');
// Enter text into specific TextField by key
await tester.enterText(find.byKey(const Key('domainTextField')), 'example.com');
// Enter text into second TextField
await tester.enterText(find.byType(TextField).at(1), '101');Scrolling
// Scroll widget into view
await tester.scrollUntilVisible(
find.byKey(const Key('targetWidget')),
500.0, // Scroll offset
);
// Drag to scroll
await tester.drag(
find.byType(ListView),
const Offset(0, -200), // Scroll down
);
await tester.pump();Async Widget Testing
Testing Loading States
testWidgets('Shows loading indicator during async operation', (tester) async {
tester.view.physicalSize = const Size(1000, 1000);
tester.view.devicePixelRatio = 1.0;
final completer = Completer<RegistrationModel>();
when(mockRepository.fetchData(any, any)).thenAnswer((_) => completer.future);
await tester.pumpWidget(createTestWidget());
await tester.enterText(find.byType(TextField).first, 'example.com');
await tester.enterText(find.byType(TextField).at(1), '101');
await tester.tap(find.text('Save'));
await tester.pump(); // Trigger the state update
// Assert loading state
expect(find.byType(CircularProgressIndicator), findsOneWidget);
// Complete the async operation
completer.complete(const RegistrationModel(status: 'success'));
await tester.pump(); // Process the completion
// Assert loaded state
expect(find.byType(CircularProgressIndicator), findsNothing);
});Using pumpAndSettle
// Wait for all animations and async operations to complete
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
// Now widget is fully rendered and stable
expect(find.byType(MyWidget), findsOneWidget);Pump vs PumpAndSettle
pump()- Advances one frame, triggers setState oncepumpAndSettle()- Waits for all animations/async to completepump(duration)- Advances by specific duration
// Use pump() for immediate state changes
await tester.tap(find.byKey(const Key('button')));
await tester.pump(); // Single frame advance
// Use pumpAndSettle() for animations/async
await tester.tap(find.byKey(const Key('button')));
await tester.pumpAndSettle(); // Wait for everything to settlePlatform-Specific Widget Testing
Testing iOS-Specific Widgets
Use addTearDown to reset the platform override — a manual reset at the end of a test is silently skipped if the test throws, leaving the override set for all subsequent tests.
testWidgets('SerialNumberWidget is shown on iOS platform', (tester) async {
tester.view.physicalSize = const Size(1000, 1000);
tester.view.devicePixelRatio = 1.0;
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
addTearDown(() => debugDefaultTargetPlatformOverride = null); // safe reset
await tester.pumpWidget(createTestWidget());
expect(find.byType(SerialNumberWidget), findsOneWidget);
expect(find.byType(CupertinoButton), findsOneWidget);
});Testing Android-Specific Widgets
testWidgets('Material widgets shown on Android', (tester) async {
tester.view.physicalSize = const Size(1000, 1000);
tester.view.devicePixelRatio = 1.0;
debugDefaultTargetPlatformOverride = TargetPlatform.android;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
await tester.pumpWidget(createTestWidget());
expect(find.byType(ElevatedButton), findsOneWidget);
expect(find.byType(MaterialApp), findsOneWidget);
});Group Platform Tests
group('Platform-Specific Tests', () {
testWidgets('iOS specific behavior', (tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
await tester.pumpWidget(createTestWidget());
expect(find.byType(CupertinoButton), findsOneWidget);
});
testWidgets('Android specific behavior', (tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
await tester.pumpWidget(createTestWidget());
expect(find.byType(ElevatedButton), findsOneWidget);
});
});Widget State Validation
Validating Text Content
// Exact match
expect(find.text('Welcome'), findsOneWidget);
// Contains text (use textContaining)
expect(find.textContaining('Welcome'), findsOneWidget);
// Multiple matches
expect(find.text('Item'), findsNWidgets(3));
// No matches
expect(find.text('Error'), findsNothing);Validating Widget Presence
// Widget exists
expect(find.byType(CircularProgressIndicator), findsOneWidget);
// Widget doesn't exist
expect(find.byType(ErrorWidget), findsNothing);
// Multiple widgets
expect(find.byType(ListTile), findsNWidgets(5));
// At least one
expect(find.byType(Card), findsWidgets);Validating Widget Properties
// Get widget and check properties
final button = tester.widget<ElevatedButton>(
find.byKey(const Key('saveButton')),
);
expect(button.enabled, true);
// Check TextField value
final textField = tester.widget<TextField>(
find.byKey(const Key('emailTextField')),
);
expect(textField.controller?.text, 'example@test.com');Testing Dialogs and Overlays
Testing Dialog Appearance
testWidgets('Shows dialog when validation fails', (tester) async {
tester.view.physicalSize = const Size(1000, 1000);
tester.view.devicePixelRatio = 1.0;
await tester.pumpWidget(createTestWidget());
await tester.enterText(find.byType(TextField).first, '');
await tester.tap(find.text('Save'));
await tester.pumpAndSettle();
expect(find.text('Please Enter Domain Name or IP Address.'), findsOneWidget);
});Dismissing Dialogs
testWidgets('Dialog can be dismissed', (tester) async {
tester.view.physicalSize = const Size(1000, 1000);
tester.view.devicePixelRatio = 1.0;
await tester.pumpWidget(createTestWidget());
// Show dialog
await tester.tap(find.byKey(const Key('showDialogButton')));
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsOneWidget);
// Dismiss dialog
await tester.tap(find.text('OK'));
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsNothing);
});Testing Navigation and Routing
Testing GoRouter Navigation
testWidgets('Save button navigates to loading screen', (tester) async {
tester.view.physicalSize = const Size(1000, 1000);
tester.view.devicePixelRatio = 1.0;
final fakeGoRouter = FakeGoRouter();
when(mockRouterManager.router).thenReturn(fakeGoRouter);
const mockResponse = RegistrationModel(status: 'success');
when(mockRepository.fetchRegistrationDetails('example.com', '101'))
.thenAnswer((_) async => mockResponse);
await tester.pumpWidget(
ProviderScope(
child: MockGoRouterProvider(
goRouter: fakeGoRouter,
child: const MaterialApp(home: RegistrationView()),
),
),
);
await tester.enterText(find.byType(TextField).first, 'example.com');
await tester.enterText(find.byType(TextField).at(1), '101');
await tester.tap(find.text('Save'));
await tester.pump();
verify(fakeGoRouter.go(AppRoute.loading.path, extra: anyNamed('extra'))).called(1);
});Testing PopScope (Back Navigation)
testWidgets('PopScope prevents back navigation when required', (tester) async {
tester.view.physicalSize = const Size(1000, 1000);
tester.view.devicePixelRatio = 1.0;
await tester.pumpWidget(
ProviderScope(
child: MockGoRouterProvider(
goRouter: fakeGoRouter,
child: const MaterialApp(
home: RegistrationView(
isItFromLaunchScreen: true,
isItFromHomeScreen: false,
),
),
),
),
);
final dynamic widgetsAppState = tester.state(find.byType(WidgetsApp));
await tester.runAsync(() async {
await widgetsAppState.didPopRoute();
});
await tester.pump();
verifyNever(fakeGoRouter.canPop());
});Testing Lifecycle Methods
Testing initState
testWidgets('initState sets up initial state correctly', (tester) async {
tester.view.physicalSize = const Size(1000, 1000);
tester.view.devicePixelRatio = 1.0;
when(mockService.isPlaying).thenReturn(true);
when(mockService.pause()).thenAnswer((_) async {});
await tester.pumpWidget(createTestWidget());
// Ensure initState and ALL its async work completes
await tester.pumpAndSettle();
// Verify initState behavior
verify(mockService.pause()).called(1);
});Testing dispose
testWidgets('dispose cleans up resources', (tester) async {
tester.view.physicalSize = const Size(1000, 1000);
tester.view.devicePixelRatio = 1.0;
when(mockRadioService.isRadioPlaying).thenReturn(true);
when(mockRadioService.pauseAudioPlayer()).thenAnswer((_) async {});
when(mockRadioService.resumeAudioPlayer()).thenAnswer((_) async {});
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
// Trigger dispose by pumping a different widget
await tester.pumpWidget(const SizedBox.shrink());
await tester.pumpAndSettle();
// Verify cleanup
verify(mockRadioService.pauseAudioPlayer()).called(1);
verify(mockRadioService.resumeAudioPlayer()).called(1);
});Common Widget Testing Patterns
Testing Conditional Rendering
When mutating global state, always restore it with addTearDown — otherwise tests that run later inherit the mutated value and produce unpredictable failures.
testWidgets('Shows widget A when condition is true', (tester) async {
tester.view.physicalSize = const Size(1000, 1000);
tester.view.devicePixelRatio = 1.0;
final original = global_variables.appCommunicationStatus;
global_variables.appCommunicationStatus = 3;
addTearDown(() => global_variables.appCommunicationStatus = original);
await tester.pumpWidget(createTestWidget());
expect(find.byType(DefaultCarouselWidget), findsOneWidget);
expect(find.byType(ImageWidget), findsNothing);
});
testWidgets('Shows widget B when condition is false', (tester) async {
tester.view.physicalSize = const Size(1000, 1000);
tester.view.devicePixelRatio = 1.0;
final original = global_variables.appCommunicationStatus;
global_variables.appCommunicationStatus = 1;
addTearDown(() => global_variables.appCommunicationStatus = original);
await tester.pumpWidget(createTestWidget());
expect(find.byType(ImageWidget), findsOneWidget);
expect(find.byType(DefaultCarouselWidget), findsNothing);
});Testing List Widgets
testWidgets('ListView displays all items', (tester) async {
tester.view.physicalSize = const Size(1000, 1000);
tester.view.devicePixelRatio = 1.0;
final items = ['Item 1', 'Item 2', 'Item 3'];
when(mockService.getItems()).thenReturn(items);
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
for (final item in items) {
expect(find.text(item), findsOneWidget);
}
});Testing Form Validation
group('Form Validation', () {
testWidgets('Shows error when email is empty', (tester) async {
tester.view.physicalSize = const Size(1000, 1000);
tester.view.devicePixelRatio = 1.0;
await tester.pumpWidget(createTestWidget());
await tester.enterText(find.byKey(const Key('emailField')), '');
await tester.tap(find.byKey(const Key('submitButton')));
await tester.pumpAndSettle();
expect(find.text('Please enter an email'), findsOneWidget);
});
testWidgets('Shows error when email is invalid', (tester) async {
tester.view.physicalSize = const Size(1000, 1000);
tester.view.devicePixelRatio = 1.0;
await tester.pumpWidget(createTestWidget());
await tester.enterText(find.byKey(const Key('emailField')), 'invalid');
await tester.tap(find.byKey(const Key('submitButton')));
await tester.pumpAndSettle();
expect(find.text('Please enter a valid email'), findsOneWidget);
});
testWidgets('Submits when form is valid', (tester) async {
tester.view.physicalSize = const Size(1000, 1000);
tester.view.devicePixelRatio = 1.0;
await tester.pumpWidget(createTestWidget());
await tester.enterText(find.byKey(const Key('emailField')), 'test@example.com');
await tester.tap(find.byKey(const Key('submitButton')));
await tester.pumpAndSettle();
verify(mockService.submit('test@example.com')).called(1);
});
});Widget Testing Checklist
Before submitting widget tests:
- [ ] Screen size set (1000x1000, DPR 1.0)
- [ ] Keys added to source widgets
- [ ] Keys used in find operations
- [ ] Both pump() and pumpAndSettle() used appropriately
- [ ] Async operations tested with Completer
- [ ] Loading states tested
- [ ] Error states tested
- [ ] Platform overrides reset (debugDefaultTargetPlatformOverride = null)
- [ ] Global state reset in tearDown
- [ ] GetIt reset in tearDown
- [ ] Widget interactions verified
- [ ] Navigation verified if applicable