Why Choose Riverpod? - Testing

Why Choose Riverpod? - Testing

This post is based off a presentation I did at Devfest 2023 in Cape Town on 23 Nov 2023. VIDEO | PRESENTATION


Previously we covered the ProviderScope, the secret sauce as I like to call it, finally we are going to take a look at just how much everything simplifies our lives when it comes to testing.


Testing is probably one of my favourite reasons to use Riverpod, just because of how simple it makes it.

Widget testing is where we bring back our ProviderScope

extension PumpApp on WidgetTester {
  Future<void> pumpApp(
    Widget widget, [
    List<Override> overrides = const [],
  ]) async {
    return pumpWidget(
      ProviderScope(
        overrides: overrides,
        child: MaterialApp(
          home: Scaffold(
            body: widget,
          ),
        ),
      ),
    );
  }
}

Above is an extension I put together to make my life easier when it comes to testing, I make use of pumpApp instead of pumpWidget as it extends on pubpWidget but also allows me to easily wrap some boilerplate around my tests.

In the above example, I am wrapping every widget that gets tested with ProviderScope and also add support for optionally passing in an array of overrides to said ProviderScope.

An extension like this can also easily include things like materialApp, Scaffold and even navigation observers.

late MockConnectivityProvider mockConnectivityProvider;

setUp(() {
 mockConnectivityProvider = MockConnectivityProvider();

 when(() => mockConnectivityProvider.isOnline).thenReturn(true);
});

testWidgets('should show OnlineIndicator', (tester) async {
  await tester.pumpApp(
    widget,
    [
      connectivityProvider.overrideWith((_) => mockConnectivityProvider),
    ],
  );

  expect(OnlineIndicator, findsOneWidget);
});

Here we have a simple examples where we testing the OnlineIndicator widget which replies on the connectivityProvider, which is a network listener to verify that the device is both connected and is able to access the Internet.

This connectivityProvider as you may have guess is also a Riverpod provider, and as you can see in the setUp function we are mocking the response from that providers isOnline method to true, aka the devise in online.

With the pumpApp we are overriding the provider with our mocked one, and then in the test we simply verify that the OnlineIndicator does in fact renders.

Mocking State

When it comes to mocking the dat from any Riverpod StateNotifier, it is about as simple:

await tester.pumpApp(
  widget,
    [
      createTripProvider.overrideWith((_) => mockCreateTripNotifier
        ..state = CreateTripState.initial().copyWith(jobs: mockJobs)),
     ],
  );

Much like above we would have setup a MockTripNotifierClass, which would look slightly different to a normal mock:

class MockCreateTripNotifier extends StateNotifier<CreateTripState>
    with Mock
    implements CreateTripNotifier {
  MockCreateTripNotifier() : super(CreateTripState());
}

Then e simply access the state directly within the override and assign it an updated value.

External Packages

Another reason I love Riverpod for testing is when it comes to external packages and their ability to allow them to be very easily mocked.

One that is extensively used throughout our application is Firestore, to normally be able to mack this it would need to be an argument on the class or function whereby the instance is passed into.

Riverpod makes that much simpler. which is why within our application we have firestoreProvider which gets overridden with FirebaseFirestore.instance within the ProviderScope during the applications startup.

As you can by now tell, this means that Firestore can be directly accessed anywhere we are making use of Riverpod.

//main
firestoreProvider.overrideWith((_) => FirebaseFirestore.instance),

//Mock
late MockDocumentReference mockDocumentReference;

mockDocumentReference = MockDocumentReference();

overrides: [
  firestoreProvider.overrideWith((_) => mockFirestore),
]

when(() => mockFirestore.doc('drivers/driver_id'))
    .thenReturn(mockDocumentReference);
when(() => mockDocumentReference.update(payload))
    .thenAnswer((_) async => Future.value());

Now when looking from a testing perspective and considering the other examples with the excerpts in the code above you can see how much simpler Riverpod makes mocking an external packages, especially ones like the Firebase packages that are exposing singletons.


I hope you found this interesting, and if you have any questions, comments, or improvements, feel free to drop a comment. Enjoy your development journey :D

Thanks for reading.