测试你的提供者程序
Riverpod API 的核心部分是能够单独测试提供者程序。
对于一个合适的测试套件,有几个挑战需要克服:
- 测试不应共享状态。这意味着新测试不应受到先前测试的影响。
- 测试应该使我们能够模拟某些功能以达到所需的状态。
- 测试环境应尽可能接近真实环境。
幸运的是,Riverpod 可以轻松实现所有这些目标。
设置测试
使用 Riverpod 定义测试时,主要有两种情况:
- 单元测试,通常没有 Flutter 依赖。 这对于单独测试提供者程序的行为非常有用。
- Widget 测试,通常带有 Flutter 依赖项。 这对于测试使用提供者程序的小部件的行为非常有用。
单元测试
单元测试是使用 package:test 中的 test
函数定义的。
与任何其他测试的主要区别在于,我们想要创建一个 ProviderContainer
对象。
此对象将使我们的测试能够与提供者程序进行交互。
建议创建一个测试实用程序来创建和处置对象 ProviderContainer
:
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;
}
然后,我们可以使用此实用程序定义一个 test
:
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'),
);
});
}
现在我们有了 ProviderContainer,我们可以使用它来读取提供者程序,使用:
container.read
,以读取提供者程序的当前值。container.listen
,以监听提供者程序并接收更改的通知。
在自动处置提供者程序时使用 container.read
时要小心。
如果您的提供者程序没有被监听,其状态可能会在测试过程中被破坏。
在这种情况下,请考虑使用 container.listen
。
无论如何,它的返回值都能读取提供者程序的当前值,
同时还能确保提供者程序不会在测试过程中被弃置:
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',
);
小部件测试
小部件测试是使用 package:flutter_test 中的 testWidgets
函数定义的。
在这种情况下,与通常的 Widget 测试的主要区别在于,
我们必须添加一个 ProviderScope
在 tester.pumpWidget
的根组件上:
void main() {
testWidgets('Some description', (tester) async {
await tester.pumpWidget(
const ProviderScope(child: YourWidgetYouWantToTest()),
);
});
}
这类似于我们在 Flutter 应用程序中启用 Riverpod 时所做的。
然后,我们可以用 tester
来与我们的小部件进行交互。
或者,如果要与提供者程序交互,可以获取 ProviderContainer
。
也可以使用 ProviderScope.containerOf(buildContext)
获得一个。
因此,通过使用 tester
,我们可以编写以下内容:
final element = tester.element(find.byType(YourWidgetYouWantToTest));
final container = ProviderScope.containerOf(element);
然后,我们可以使用它来读取提供者程序。下面是一个完整的示例:
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',
);
});
}
模拟提供者程序
到目前为止,我们已经了解了如何设置测试以及与提供者程序的基本交互。 但是,在某些情况下,我们可能想要模拟一个提供者程序。
很酷的部分:默认情况下可以模拟所有提供者程序,无需任何额外设置。
这可以通过在 ProviderScope
或 ProviderContainer
上指定 overrides
参数来实现。
请考虑以下提供者程序:
// An eagerly initialized provider.
Future<String> example(Ref ref) async => 'Hello world';
我们可以模拟它通过以下方式:
// 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(),
),
);
监视提供者程序中的更改
由于我们在测试中获得了一个 ProviderContainer
,因此可以使用它来“监听”提供者程序:
container.listen<String>(
provider,
(previous, next) {
print('The provider changed from $previous to $next');
},
);
然后,您可以将其与 mockito
或 mocktail 等包结合使用,以使用它们的 verify
API。
或者更简单地说,您可以在列表中添加所有更改并对其进行断言。
等待异步提供者程序
在 Riverpod 中,提供者程序返回 Future/Stream 是很常见的。
在这种情况下,我们的测试可能需要等待异步操作完成。
一种方法是读取提供者程序的 .future
:
// 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'),
);
模拟通知者程序
通常不鼓励嘲笑通知者程序。
相反,您可能应该在通告程序的逻辑中引入一个抽象级别,以便您可以模拟该抽象。
例如,与其模拟通告程序,不如模拟通告程序用来从中获取数据的“存储库”。
如果您坚持要模拟通告程序,那么创建这样的通告程序需要特别注意: 您的模拟必须对原始通告程序基类进行子类化: 您不能“实现”通告程序,因为这会破坏接口。
因此,在模拟通告程序时,不要编写以下模拟代码:
class MyNotifierMock with Mock implements MyNotifier {}
你应该改写:
class MyNotifier extends _$MyNotifier {
int build() => throw UnimplementedError();
}
// Your mock needs to subclass the Notifier base-class corresponding
// to whatever your notifier uses
class MyNotifierMock extends _$MyNotifier with Mock implements MyNotifier {}
为此,您的模拟必须与您模拟的通知者程序放在同一个文件中。
否则,您将无法访问该 _$MyNotifier
类。