К содержимому

Testing your providers

A core part of the Riverpod API is the ability to test your providers in isolation.

For a proper test suites, there are a few challenges to overcome:

  • Tests should not share state. This means that new tests should not be affected by the previous tests.
  • Tests should give us the ability to mock certain functionalities, to achieve the desired state.
  • The test environment should be as close as possible to the real environment.

Fortunately, Riverpod makes it easy to achieve all of these goals.

Setting up a test

When defining a test with Riverpod, there are two main scenarios:

  • Unit tests, usually with no Flutter dependency. This can be useful for testing the behavior of a provider in isolation.
  • Widget tests, usually with a Flutter dependency. This can be useful for behavior of a widget using a provider.

Unit tests

Unit tests are defined using the test function from package:test.

The main difference with any other test is that we will want to create a ProviderContainer object. This object will enable our test to interact with providers.

It encouraged to make a testing utility for both creating and disposing of a ProviderContainer object:

import 'package:riverpod/riverpod.dart';
import 'package:test/test.dart';

/// A testing utility which creates a [ProviderContainer] and automatically
/// disposes it at the end of the test.
ProviderContainer createContainer({
ProviderContainer? parent,
List<Override> overrides = const [],
List<ProviderObserver>? observers,
}) {
// Create a ProviderContainer, and optionally allow specifying parameters.
final container = ProviderContainer(
parent: parent,
overrides: overrides,
observers: observers,
);

// When the test ends, dispose the container.
addTearDown(container.dispose);

return container;
}

Then, we can define a test using this utility:

void main() {
test('Some description', () {
// Create a ProviderContainer for this test.
// DO NOT share ProviderContainers between tests.
final container = createContainer();

// TODO: use the container to test your application.
expect(
container.read(provider),
equals('some value'),
);
});
}

Now that we have a ProviderContainer, we can use it to read providers using:

  • container.read, to read the current value of a provider.
  • container.listen, to listen to a provider and be notified of changes.
предупреждение

Be careful when using container.read when providers are automatically disposed.
If your provider is not listened, chances are its state will get destroyed in the middle of your test.

In that case, consider using container.listen.
Its return value enables reading the current value of provider anyway, but will also ensure that the provider is not disposed in the middle of your test:

final subscription = container.listen<String>(provider, (_, __) {});

expect(
// Equivalent to `container.read(provider)`
// But the provider will not be disposed unless "subscription" is disposed.
subscription.read(),
'Some value',
);

Widget tests

Widget tests are defined using the testWidgets function from package:flutter_test.

In this case, the main difference with usual Widget tests is that we must add a ProviderScope widget at the root of tester.pumpWidget:

void main() {
testWidgets('Some description', (tester) async {
await tester.pumpWidget(
const ProviderScope(child: YourWidgetYouWantToTest()),
);
});
}

This is similar to what we do when we enable Riverpod in our Flutter app.

Then, we can use tester to interact with our widget. Alternatively if you want to interact with providers, you can obtain a ProviderContainer. One can be obtained using ProviderScope.containerOf(buildContext).
By using tester, we can therefore write the following:

final element = tester.element(find.byType(YourWidgetYouWantToTest));
final container = ProviderScope.containerOf(element);

We can then use it to read providers. Here's a full example:

void main() {
testWidgets('Some description', (tester) async {
await tester.pumpWidget(
const ProviderScope(child: YourWidgetYouWantToTest()),
);

final element = tester.element(find.byType(YourWidgetYouWantToTest));
final container = ProviderScope.containerOf(element);

// TODO interact with your providers
expect(
container.read(provider),
'some value',
);
});
}

Mocking providers

So far, we've seen how to setup a test and to basic interactions with providers. However, in some cases, we may want to mock a provider.

The cool part: All providers can be mocked by default, without any additional setup.
This is possible by specifying the overrides parameter on either ProviderScope or ProviderContainer.

Consider the following provider:

// An eagerly initialized provider.

Future<String> example(ExampleRef ref) async => 'Hello world';

We can mock it using:

// In unit tests, by reusing our previous "createContainer" utility.
final container = createContainer(
// We can specify a list of providers to mock:
overrides: [
// In this case, we are mocking "exampleProvider".
exampleProvider.overrideWith((ref) {
// This function is the typical initialization function of a provider.
// This is where you normally call "ref.watch" and return the initial state.

// Let's replace the default "Hello world" with a custom value.
// Then, interacting with `exampleProvider` will return this value.
return 'Hello from tests';
}),
],
);

// We can also do the same thing in widget tests using ProviderScope:
await tester.pumpWidget(
ProviderScope(
// ProviderScopes have the exact same "overrides" parameter
overrides: [
// Same as before
exampleProvider.overrideWith((ref) => 'Hello from tests'),
],
child: const YourWidgetYouWantToTest(),
),
);

Spying on changes in a provider

Since we obtained a ProviderContainer in our tests, it is possible to use it to "listen" to a provider:

container.listen<String>(
provider,
(previous, next) {
print('The provider changed from $previous to $next');
},
);

You can then combine this with packages such as mockito or mocktail to use their verify API.
Or more simply, you can add all changes in a list and assert on it.

Awaiting for asynchronous providers

In Riverpod, it is very common for providers to return a Future/Stream.
In that case, chances are our tests wants to await for that asynchronous operation to be completed.

One way to do so is to read the .future of a provider:

// TODO: use the container to test your application.
// Our expectation is asynchronous, so we should use "expectLater"
await expectLater(
// We read "provider.future" instead of "provider".
// This is possible on asynchronous providers, and returns a future
// which will resolve with the value of the provider.
container.read(provider.future),
// We can verify that the future resolves with the expected value.
// Alternatively we can use "throwsA" for errors.
completion('some value'),
);