[译][官方文档] 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 类。


相关推荐
JarvanMo8 小时前
关于Flutter架构的小小探讨
前端·flutter
顾林海8 小时前
Flutter 图标和按钮组件
android·开发语言·前端·flutter·面试
yzwdzkn9 小时前
解决Flutter 2.10.5在升级Xcode 16后的各种报错
flutter·macos·xcode
亚洲小炫风12 小时前
flutter json解析增强
flutter·json·json兼容格式
SY.ZHOU12 小时前
Flutter 与原生通信
android·flutter·ios
louisgeek15 小时前
Dart Stream 的 2 种类型
dart
恋猫de小郭15 小时前
IntelliJ IDEA 2025.1 发布 ,默认 K2 模式 | Android Studio 也将跟进
android·前端·flutter
梦想不只是梦与想16 小时前
鸿蒙系统开发状态更新字段区别对比
android·java·flutter·web·鸿蒙
RichardLai8816 小时前
[Flutter学习之Dart基础] - 集合(List, Set,Map)
android·flutter
bst@微胖子16 小时前
Flutter项目之设置页
android·javascript·flutter