Testing Riverpod StateNotifierProvider

An example of how to mock state for testing widgets when using riverpod StateNotifierProvider.

These tests work without Mocktail / Mockito

Quick Details

  • Model: Counter class created with Freezed
  • State: CounterNotifier extends StateNotifier
  • Provider: counterProvider is a StateNotifierProvider

Important For Testability of StateNotifierProvider

When testing Flutter widgets, one may want to mock a predefined state in order to test how widgets render for a given state. For example, a widget may need to show or hide depending on user role, which could be part of a user object stored in state. Our test allows us to ensure this important conditional rendering does not break with future code changes.

In this simplified example, we are using a counter to test whether a widget renders when the count is even.

This repository exists as an example for how to architect your StateNotifier and its constructor such that a mocked provider and predefined state can be used by the test widget.

Below is a breakdown of what is happening in this repository.

Explanation

The Test Widget

Widget isEvenTestWidget(StateNotifierProvider<CounterNotifier, Counter> mockProvider) {
    return ProviderScope(
      overrides: [
        counterProvider.overrideWithProvider(mockProvider),
      ],
      child: const MaterialApp(
        home: ScreenHome(),
      ),
    );
  }

This test widget for our home screen uses the overrides property of ProviderScope() in order to override the provider used in the widget.

When the home.dart ScreenHome() widget calls Counter counter = ref.watch(counterProvider); it will use our mockProvider instead of the “real” provider.

The isEvenTestWidget() mockProvider argument is the same “type” of provider as counterProvider().

The Test

testWidgets('If count is even, IsEvenMessage is rendered.', (tester) async {
  // Mock a provider with an even count
  final mockCounterProvider =
      StateNotifierProvider<CounterNotifier, Counter>((ref) => CounterNotifier(counter: const Counter(count: 2)));

  await tester.pumpWidget(isEvenTestWidget(mockCounterProvider));

  expect(find.byType(IsEvenMessage), findsOneWidget);
});

In the test, we create a mockProvider with predefined values that we need for testing ScreenHome() widget rendering. In this example, our provider is initialized with the state count: 2.

We are testing that the isEvenMessage() widget is rendered with an even count (of 2). Another test tests that the widget is not rendered with an odd count.

StateNotifier Constructor

class CounterNotifier extends StateNotifier<Counter> {
  CounterNotifier({Counter counter = const Counter(count: 0)}) : super(counter);

  void increment() {
    state = state.copyWith(count: state.count + 1);
  }
}

In order to be able to create a mockProvider with a predefined state, it is important that the StateNotifier (counter_state.dart) constructor includes an optional parameter of the state model. The default argument is how the state should normally initialize. Our tests can optionally provide a specified state for testing which is passed to super().


Build Runner

Code generator is required for freezed class code generation

$ flutter pub run build_runner build --delete-conflicting-outputs

GitHub

View Github