Learn how to write comprehensive tests for your Riverpod providers using ProviderContainer, overrides, and mocking strategies.
Use ProviderContainer to test providers in isolation without widget dependencies:
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);
});
});
}Test async providers by awaiting their futures and handling loading/error states:
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>());
});
});
}Use provider overrides to inject mock dependencies and control provider behavior in tests:
// 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>()),
);
});
});
}Test widgets that consume providers using ProviderScope:
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);
});
});
}Monitor provider state changes using listeners and expect state transitions:
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();
});
});
}Test multiple providers working together and complex interactions:
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);
});
});
}Save provider state snapshots and compare against future runs to detect unintended changes.
Use libraries like test_api to generate random inputs and verify provider invariants.
Measure provider creation time and memory usage to catch performance regressions.
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.