[译][官方文档] Flutter/Dart 状态管理库 Riverpod (十三)- 概要 - 测试 provider

原文链接: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 不会在测试过程中被清除:

dart 复制代码
final 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 的,无需多余的设置。

通过对 ProviderScopeProviderContainer 指定 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');
  },
);

然后可以将其和一些测试包(如 mockitomocktail)进行绑定以使用这些包的 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 类。


相关推荐
君蓦4 小时前
Flutter 本地存储与数据库的使用和优化
flutter
problc13 小时前
Flutter中文字体设置指南:打造个性化的应用体验
android·javascript·flutter
lqj_本人1 天前
鸿蒙next选择 Flutter 开发跨平台应用的原因
flutter·华为·harmonyos
lqj_本人1 天前
Flutter&鸿蒙next 状态管理框架对比分析
flutter·华为·harmonyos
起司锅仔1 天前
Flutter启动流程(2)
flutter
hello world smile1 天前
最全的Flutter中pubspec.yaml及其yaml 语法的使用说明
android·前端·javascript·flutter·dart·yaml·pubspec.yaml
lqj_本人1 天前
Flutter 的 Widget 概述与常用 Widgets 与鸿蒙 Next 的对比
flutter·harmonyos
iFlyCai1 天前
极简实现酷炫动效:Flutter隐式动画指南第二篇之一些酷炫的隐式动画效果
flutter
lqj_本人1 天前
Flutter&鸿蒙next 中使用 MobX 进行状态管理
flutter·华为·harmonyos