Testing Riverpod Providers

Learn how to write comprehensive tests for your Riverpod providers using ProviderContainer, overrides, and mocking strategies.

Basic Provider Testing

Use ProviderContainer to test providers in isolation without widget dependencies:

counter_provider_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  group('Counter Provider Tests', () {
    late ProviderContainer container;

    setUp(() {
      container = ProviderContainer();
    });

    tearDown(() {
      container.dispose();
    });

    test('initial state is 0', () {
      expect(container.read(counterProvider), 0);
    });

    test('increment increases count', () {
      container.read(counterProvider.notifier).increment();
      expect(container.read(counterProvider), 1);
    });

    test('decrement decreases count', () {
      container.read(counterProvider.notifier).increment();
      container.read(counterProvider.notifier).increment();
      expect(container.read(counterProvider), 2);

      container.read(counterProvider.notifier).decrement();
      expect(container.read(counterProvider), 1);
    });

    test('reset sets count to 0', () {
      container.read(counterProvider.notifier).increment();
      container.read(counterProvider.notifier).reset();
      expect(container.read(counterProvider), 0);
    });
  });
}

Async Provider Testing

Test async providers by awaiting their futures and handling loading/error states:

user_provider_test.dart
import 'package:flutter_test/flutter_test.dart';

void main() {
  group('User Provider Tests', () {
    late ProviderContainer container;

    setUp(() {
      container = ProviderContainer();
    });

    tearDown(() {
      container.dispose();
    });

    test('fetches user successfully', () async {
      // Trigger the provider
      final future = container.read(userProvider(123).future);
      
      // Wait for completion
      final user = await future;
      
      expect(user.id, 123);
      expect(user.name, isNotEmpty);
    });

    test('handles API errors correctly', () async {
      // Test with invalid ID that triggers error
      expect(
        () => container.read(userProvider(-1).future),
        throwsA(isA<Exception>()),
      );
    });

    test('loading state is handled', () {
      final asyncValue = container.read(userProvider(123));
      
      expect(asyncValue, isA<AsyncLoading>());
    });
  });
}

Mocking Dependencies with Overrides

Use provider overrides to inject mock dependencies and control provider behavior in tests:

user_service_test.dart
// Mock repository
class MockUserRepository extends Mock implements UserRepository {}

void main() {
  group('User Service Tests', () {
    late MockUserRepository mockRepository;
    late ProviderContainer container;

    setUp(() {
      mockRepository = MockUserRepository();
      container = ProviderContainer(
        overrides: [
          // Override the repository provider with mock
          userRepositoryProvider.overrideWithValue(mockRepository),
        ],
      );
    });

    tearDown(() {
      container.dispose();
    });

    test('getUserProfile calls repository correctly', () async {
      // Arrange
      final expectedUser = User(id: 1, name: 'John');
      when(mockRepository.getUser(1))
          .thenAnswer((_) => Future.value(expectedUser));

      // Act
      final user = await container.read(userProfileProvider(1).future);

      // Assert
      expect(user, expectedUser);
      verify(mockRepository.getUser(1)).called(1);
    });

    test('handles repository errors', () async {
      // Arrange
      when(mockRepository.getUser(1))
          .thenThrow(Exception('User not found'));

      // Act & Assert
      expect(
        () => container.read(userProfileProvider(1).future),
        throwsA(isA<Exception>()),
      );
    });
  });
}

Widget Testing with Providers

Test widgets that consume providers using ProviderScope:

counter_widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  group('Counter Widget Tests', () {
    testWidgets('displays initial count', (tester) async {
      await tester.pumpWidget(
        ProviderScope(
          child: MaterialApp(
            home: CounterWidget(),
          ),
        ),
      );

      expect(find.text('0'), findsOneWidget);
    });

    testWidgets('increment button increases count', (tester) async {
      await tester.pumpWidget(
        ProviderScope(
          child: MaterialApp(
            home: CounterWidget(),
          ),
        ),
      );

      // Tap increment button
      await tester.tap(find.text('+'));
      await tester.pump();

      expect(find.text('1'), findsOneWidget);
    });

    testWidgets('uses mocked provider', (tester) async {
      final mockCounter = MockCounter();
      when(mockCounter.value).thenReturn(42);

      await tester.pumpWidget(
        ProviderScope(
          overrides: [
            counterProvider.overrideWith((ref) => mockCounter),
          ],
          child: MaterialApp(
            home: CounterWidget(),
          ),
        ),
      );

      expect(find.text('42'), findsOneWidget);
    });
  });
}

Testing State Changes

Monitor provider state changes using listeners and expect state transitions:

state_change_test.dart
void main() {
  group('State Change Tests', () {
    test('listener receives state updates', () {
      final container = ProviderContainer();
      final states = <int>[];

      // Listen to state changes
      container.listen(
        counterProvider,
        (previous, next) => states.add(next),
      );

      // Trigger state changes
      container.read(counterProvider.notifier).increment();
      container.read(counterProvider.notifier).increment();
      container.read(counterProvider.notifier).reset();

      // Verify all state transitions
      expect(states, [1, 2, 0]);
      
      container.dispose();
    });

    test('async state transitions', () async {
      final container = ProviderContainer();
      final states = <AsyncValue<User>>[];

      container.listen(
        userProvider(123),
        (previous, next) => states.add(next),
      );

      // Wait for async completion
      await container.read(userProvider(123).future);

      expect(states.first, isA<AsyncLoading>());
      expect(states.last, isA<AsyncData>());
      
      container.dispose();
    });
  });
}

Integration Testing

Test multiple providers working together and complex interactions:

shopping_cart_test.dart
void main() {
  group('Shopping Cart Integration', () {
    late ProviderContainer container;

    setUp(() {
      container = ProviderContainer();
    });

    tearDown(() {
      container.dispose();
    });

    test('adding products updates cart and total', () {
      final product = Product(id: '1', name: 'Widget', price: 10.0);
      
      // Add product to cart
      container.read(cartProvider.notifier).addProduct(product);
      
      // Verify cart contents
      final cartItems = container.read(cartProvider);
      expect(cartItems.length, 1);
      expect(cartItems.first.product.id, '1');
      expect(cartItems.first.quantity, 1);
      
      // Verify computed total
      final total = container.read(cartTotalProvider);
      expect(total, 10.0);
    });

    test('removing products updates total', () {
      final product = Product(id: '1', name: 'Widget', price: 10.0);
      
      // Add and remove product
      container.read(cartProvider.notifier).addProduct(product);
      container.read(cartProvider.notifier).removeProduct('1');
      
      // Verify empty cart
      expect(container.read(cartProvider), isEmpty);
      expect(container.read(cartTotalProvider), 0.0);
    });
  });
}

Testing Best Practices

Do

  • Test providers in isolation using ProviderContainer
  • Use overrides to mock dependencies
  • Test both success and error scenarios
  • Verify state transitions and side effects

Don't

  • Test UI and provider logic in the same test
  • Forget to dispose ProviderContainer
  • Rely on real network calls in unit tests
  • Test implementation details of generated code

Common Testing Patterns

Golden Master Testing

Save provider state snapshots and compare against future runs to detect unintended changes.

Property-Based Testing

Use libraries like test_api to generate random inputs and verify provider invariants.

Performance Testing

Measure provider creation time and memory usage to catch performance regressions.

Testing Tools

Consider using additional testing tools like mockito for mocks, fake_async for time-based testing, and integration_test for end-to-end scenarios in complex applications.