mobile bluesky app made with flutter lazurite.stormlightlabs.org/
mobile bluesky flutter

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 in build() with .catchError() or try/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":

  1. Flush Widget Tree: Force a rebuild with a simple widget before the test ends.

    await tester.pumpWidget(const Placeholder());
    
  2. Synchronous DB Streams: When using Drift in tests, ensure streams close synchronously.

    final connection = DatabaseConnection(executor, closeStreamsSynchronously: true);
    
  3. 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