Testing Patterns#
Advanced patterns and techniques for testing Lazurite components. Supplementing the basics at README.md.
Testing AutoDispose Providers#
When testing providers marked with @riverpod (which are autoDispose by default) or explicitly .autoDispose:
Rule: Always maintain an active listener using container.listen() when testing autoDispose providers, especially when awaiting async operations.
Anti-Pattern:
// DON'T: Provider may be disposed immediately if it has no listeners
await container.read(provider.notifier).method();
Correct Pattern:
// DO: Keep provider alive
container.listen(provider, (_, _) {});
await container.read(provider.notifier).method();
Handling Async Initialization#
Providers that use Future.microtask in their build() method to trigger side effects can cause unhandled exceptions in tests.
Solution:
- Wrap
refresh()or side-effect calls inbuild()with.catchError()ortry/catch. - Ensure tests verify behavior when these initial calls fail.
Async Disposal#
If a provider or service requires async cleanup, await it before disposing the container.
tearDown(() async {
await container.read(serviceProvider).dispose();
container.dispose();
});
Fixing Pending Timer Errors#
If a test fails with "Timer is still pending after widget tree was disposed":
-
Flush Widget Tree: Force a rebuild with a simple widget before the test ends.
await tester.pumpWidget(const Placeholder()); -
Synchronous DB Streams: When using Drift in tests, ensure streams close synchronously.
final connection = DatabaseConnection(executor, closeStreamsSynchronously: true); -
Override Background Controllers: Replace background sync/cleanup controllers with no-ops.
overrides: [ timelineCleanupControllerProvider.overrideWith((ref) {}), ],
Drift Database Testing#
Integration Tests: Use NativeDatabase.memory() to test actual DAO logic.
Widget Tests: ALWAYS use MockAppDatabase. Never use a real database in widget tests to avoid background isolate and timer issues.
Mocking Streams#
Rule: Never use Stream.empty() for List streams. Use Stream.value([]) to ensure the first value is emitted immediately. Stream.empty() never emits, which can cause await container.read(provider.future) to hang indefinitely if the provider waits for the first value.
// Correct: emits empty list immediately
when(() => mockDao.watchItems()).thenAnswer((_) => Stream.value([]));
// Wrong: never emits, causes test to hang
when(() => mockDao.watchItems()).thenAnswer((_) => Stream.empty());
Widget Test Setup#
Use reusable "subject builders" to standardize test environments.
Reference: createSubject patterns in test/src/features/ (e.g., search_screen_test.dart).
Widget createSubject({
List<Override> overrides = const [],
}) {
return ProviderScope(
overrides: [
appDatabaseProvider.overrideWithValue(mockDatabase),
...overrides,
],
child: const MaterialApp(home: MyScreen()),
);
}
References#
- Mocks:
test/helpers/mocks.dart - Pump App Helper:
test/helpers/pump_app.dart - Database Test Setup:
test/helpers/test_database.dart