原文链接:Testing your providers | Riverpod
pub:riverpod | Dart Package (flutter-io.cn)
译时版本: 2.4.9
之前翻译过 Riverpod 的官方文档,现在随着版本更新,官方文档又多了很多新内容,所以再补充翻译一下。
之前翻译过的内容,现在官方文档有中文了。
Flutter状态管理库Riverpod官方文档翻译汇总 - 掘金 (juejin.cn)
测试 provider
Riverpod API 的一个核心部分是独立测试 provider 的能力。
对于一个合适的测试集,需要克服一些挑战:
- 测试不应共享状态。这意味着新的测试不应当受到前一个测试的影响。
- 测试应该赋予测试者对特定功能进行 Mock 以实现期望状态的能力。
- 测试环境应该尽可能接近真实环境。
幸运的是,Riverpod 使实现这些目标变得简单。
建立测试
使用 Riverpod 定义测试时,有两个主要方案:
- 单元测试,通常无需 Flutter 依赖。这对于独立测试 provider 的行为很有用。
- 组件测试,通常需要 Flutter 依赖。对于测试使用了 provider 的组件的行为很有用。
单元测试
单元测试使用 package:test 中的 test
函数定义。
和其它测试的主要区别是会创建一个 ProviderContainer
对象。该对象能使测试和 provider 交互。
鼓励同时编写用于创建和清除 ProviderContainer
对象的测试工具(函数):
dart
import 'package:riverpod/riverpod.dart';
import 'package:test/test.dart';
/// 创建[ProviderContainer]并在测试最后将其清除的测试工具(函数)。
ProviderContainer createContainer({
ProviderContainer? parent,
List<Override> overrides = const [],
List<ProviderObserver>? observers,
}) {
// 创建一个 ProviderContainer ,允许指定参数是可选项。
final container = ProviderContainer(
parent: parent,
overrides: overrides,
observers: observers,
);
// 测试完成时,清除 container 。
addTearDown(container.dispose);
return container;
}
然后可以使用该工具(函数)定义一个 test
:
dart
void main() {
test('Some description', () {
// 为该测试创建一个 ProviderContainer 。
// !不要! 在测试之间共享 ProviderContainers 。
final container = createContainer();
// TODO: 使用 container 测试应用。
expect(
container.read(provider),
equals('some value'),
);
});
}
现在已经有了 ProviderContainer ,可以如下用其读取 provider :
container.read
,读取 provider 的当前值。container.listen
,监听 provider ,provider 发生改变时会接收到通知。
警告
使用
container.read
时要注意 provider 自动释放的处理。如果 provider 没有被监听,很可能在测试过程中状态会被消除。
这种情况下,考虑使用
container.listen
。它的返回值都能读取 provider 的当前值,但也能保证 provider 不会在测试过程中被清除:
dartfinal subscription = container.listen<String>(provider, (_, __) {}); expect( // 等同于 `container.read(provider)` // 但是 provider 不会被清除,除非 "subscription" (订阅)已经清除。 subscription.read(), 'Some value', );
组件测试
组件测试使用 package:flutter_test 中的 testWidgets
函数定义。
这种情况和一般组件测试的主要区别是必须在 tester.pumpWidget
的根节点添加 ProviderScope
组件:
dart
void main() {
testWidgets('Some description', (tester) async {
await tester.pumpWidget(
const ProviderScope(child: YourWidgetYouWantToTest()),
);
});
}
这和在 Flutter APP 中启用 Riverpod 时所做的是类似的。
然后使用 tester
和组件进行交互。或者,如果要和 provider 进行交互,可以获取 ProviderContainer
。使用 ProviderScope.containerOf(buildContext)
可以获取到一个 ProviderContainer
。
使用 tester
,这样可以如下编写:
dart
final element = tester.element(find.byType(YourWidgetYouWantToTest));
final container = ProviderScope.containerOf(element);
然后可以用其读取 provider 。这里是完整的示例:
dart
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 和 provider 进行交互。
expect(
container.read(provider),
'some value',
);
});
}
对 provider 进行 Mock
迄今为止,看到了如何建立测试和如何与 provider 进行交互。尽管如此,一些场合也需要对 provider 进行 Mock 。
很酷的是:所有的 provider 默认都是可被 Mock 的,无需多余的设置。
通过对 ProviderScope
或 ProviderContainer
指定 overrides
参数即可实现。
看一下下面的 provider :
dart
// 饿汉式初始化的 provider 。
@riverpod
Future<String> example(ExampleRef ref) async => 'Hello world';
可以如下 Mock :
dart
// 在单元测试中,使用前面的 "createContainer" 工具(函数)。
final container = createContainer(
// 指定需要 Mock 的 provider 列表:
overrides: [
// 该示例中,是对 "exampleProvider" 进行 Mock 。
exampleProvider.overrideWith((ref) {
// 该函数是典型的 provider 的初始化函数。
// 平常是调用 "ref.watch" 并返回初始状态的地方。
// 现在用一个自定义值替换默认的 "Hello world" 。
// 然后和 `exampleProvider` 交互会返回该值。
return 'Hello from tests';
}),
],
);
// 也可在组件测试中使用 ProviderScope 做同样的处理:
await tester.pumpWidget(
ProviderScope(
// ProviderScope 同样也有 "overrides" 参数。
overrides: [
// 和前面一样
exampleProvider.overrideWith((ref) => 'Hello from tests'),
],
child: const YourWidgetYouWantToTest(),
),
);
在 provider 中监视变化
由于在测试中获取到了 ProviderContainer
,然后就能用其"监听" provider 了:
dart
container.listen<String>(
provider,
(previous, next) {
print('The provider changed from $previous to $next');
},
);
然后可以将其和一些测试包(如 mockito 或 mocktail)进行绑定以使用这些包的 verify
API 。
简单来说,可以在列表中添加所有的变化然后进行断言。
等待异步 provider
在 Riverpod 中,provider 返回 Future/Stream 是很常见的。
这些情况下,很可能测试需要等待异步操作完成。
一种做法是读取 provider 的 .future
:
dart
// TODO: 使用 container 测试应用。
// 期望值是异步的,所以应该使用 "expectLater"
await expectLater(
// 读取 "provider.future" 而不是 "provider"。
// 对于异步 provider 这是可以的,然后返回和 provider 的值相关联的 future 。
container.read(provider.future),
// 然后就能使用期望值的 future 进行验证了。
// 另一个选择是对于错误使用 "throwsA" 。
completion('some value'),
);
对 Notifier 进行 Mock
通常不鼓励对 Notifier 进行 Mock 。
相反,应该在 Notifier 中引入一个抽象层,这样就能对抽象层进行 Mock 了。
例如,不对 Notifier 进行 Mock ,对 Notifier 用来获取数据的 "repository" 进行 Mock 。
如果坚持要对 Notifier 进行 Mock ,对于创建这样的 mock , 有一个需要特别考虑的事: Mock 必须是原始 Notifier 基类的子类: 无法"实现" Notifier ,因为这会破坏接口。
因此,对 Notifier 进行 Mock 时,不是编写下面的 mockito 代码:
dart
class MyNotifierMock with Mock implements MyNotifier {}
应该改写为:
dart
@riverpod
class MyNotifier extends _$MyNotifier {
@override
int build() => throw UnimplementedError();
}
// mock 必须根据代码中 notifier 使用内容的 Notifier 基类的子类。
class MyNotifierMock extends _$MyNotifier with Mock implements MyNotifier {}
要使此代码正常运行, mock 代码需要和被 Mock 的 Notifier 放在同一个文件中。 否则应该无法访问 _$MyNotifier
类。