跳到主要内容

测试你的提供者程序

Riverpod API 的核心部分是能够单独测试提供者程序。

对于一个合适的测试套件,有几个挑战需要克服:

  • 测试不应共享状态。这意味着新测试不应受到先前测试的影响。
  • 测试应该使我们能够模拟某些功能以达到所需的状态。
  • 测试环境应尽可能接近真实环境。

幸运的是,Riverpod 可以轻松实现所有这些目标。

设置测试

使用 Riverpod 定义测试时,主要有两种情况:

  • 单元测试,通常没有 Flutter 依赖。 这对于单独测试提供者程序的行为非常有用。
  • Widget 测试,通常带有 Flutter 依赖项。 这对于测试使用提供者程序的小部件的行为非常有用。

单元测试

单元测试是使用 package:test 中的 test 函数定义的。

与任何其他测试的主要区别在于,我们想要创建一个 ProviderContainer 对象。 此对象将使我们的测试能够与提供者程序进行交互。

建议创建一个测试实用程序来创建和处置对象 ProviderContainer

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,
);

// 测试结束后,处置容器。
addTearDown(container.dispose);

return container;
}

然后,我们可以使用此实用程序定义一个 test

void main() {
test('Some description', () {
// 为该测试创建一个 ProviderContainer。
// 切勿!在测试之间共享 ProviderContainer。
final container = createContainer();

// TODO: 使用容器测试你的应用程序。
expect(
container.read(provider),
equals('some value'),
);
});
}

现在我们有了 ProviderContainer,我们可以使用它来读取提供者程序,使用:

  • container.read,以读取提供者程序的当前值。
  • container.listen,以监听提供者程序并接收更改的通知。
警告

在自动处置提供者程序时使用 container.read 时要小心。
如果您的提供者程序没有被监听,其状态可能会在测试过程中被破坏。

在这种情况下,请考虑使用 container.listen
无论如何,它的返回值都能读取提供者程序的当前值, 同时还能确保提供者程序不会在测试过程中被弃置:

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

expect(
// 等同于 `container.read(provider)`
// 但除非处置了 "subscription",否则不会处置提供者程序。
subscription.read(),
'Some value',
);

小部件测试

小部件测试是使用 package:flutter_test 中的 testWidgets 函数定义的。

在这种情况下,与通常的 Widget 测试的主要区别在于, 我们必须添加一个 ProviderScopetester.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 与你的提供者程序交互
expect(
container.read(provider),
'some value',
);
});
}

模拟提供者程序

到目前为止,我们已经了解了如何设置测试以及与提供者程序的基本交互。 但是,在某些情况下,我们可能想要模拟一个提供者程序。

很酷的部分:默认情况下可以模拟所有提供者程序,无需任何额外设置。
这可以通过在 ProviderScopeProviderContainer 上指定 overrides 参数来实现。

请考虑以下提供者程序:

// 一个急于初始化的提供者程序。

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

我们可以模拟它通过以下方式:

    // 在单元测试中,重用我们之前的 "createContainer "工具。
final container = createContainer(
// 我们可以指定要模拟的提供者程序列表:
overrides: [
// 在本例中,我们模拟的是 "exampleProvider"。
exampleProvider.overrideWith((ref) {
// 该函数是典型的提供者程序初始化函数。
// 通常在此调用 "ref.watch "并返回初始状态。

// 让我们用自定义值替换默认的 "Hello world"。
// 然后,与 `exampleProvider` 交互时将返回此值。
return 'Hello from tests';
}),
],
);

// 我们还可以使用 ProviderScope 在 widget 测试中做同样的事情:
await tester.pumpWidget(
ProviderScope(
// ProviderScopes 具有完全相同的 "overrides" 参数
overrides: [
// 和之前一样
exampleProvider.overrideWith((ref) => 'Hello from tests'),
],
child: const YourWidgetYouWantToTest(),
),
);

监视提供者程序中的更改

由于我们在测试中获得了一个 ProviderContainer,因此可以使用它来“监听”提供者程序:

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

然后,您可以将其与 mockitomocktail 等包结合使用,以使用它们的 verify API。
或者更简单地说,您可以在列表中添加所有更改并对其进行断言。

等待异步提供者程序

在 Riverpod 中,提供者程序返回 Future/Stream 是很常见的。
在这种情况下,我们的测试可能需要等待异步操作完成。

一种方法是读取提供者程序的 .future

    // TODO: 使用容器来测试您的应用程序。
// 我们的期望是异步的,所以应该使用 "expectLater"(期望稍后)。
await expectLater(
// 我们读取的是 "provider.future",而不是 "provider"。
// 这在异步提供者程序上是可能发生的,
// 并返回一个携带了提供者程序的值的 Future。
container.read(provider.future),
// We can verify that the future resolves with the expected value.
// Alternatively we can use "throwsA" for errors.
// 我们可以验证 Future 是否按预期值解析。
// 或者,我们可以使用 "throwsA" 来处理错误。
completion('some value'),
);

模拟通知者程序

通常不鼓励嘲笑通知者程序。
相反,您可能应该在通告程序的逻辑中引入一个抽象级别,以便您可以模拟该抽象。 例如,与其模拟通告程序,不如模拟通告程序用来从中获取数据的“存储库”。

如果您坚持要模拟通告程序,那么创建这样的通告程序需要特别注意: 您的模拟必须对原始通告程序基类进行子类化: 您不能“实现”通告程序,因为这会破坏接口。

因此,在模拟通告程序时,不要编写以下模拟代码:

class MyNotifierMock with Mock implements MyNotifier {}

你应该改写:


class MyNotifier extends _$MyNotifier {

int build() => throw UnimplementedError();
}

// 您的模拟类需要作为 Notifier 的子类,与您的通知者程序使用的基类相对应
class MyNotifierMock extends _$MyNotifier with Mock implements MyNotifier {}

为此,您的模拟必须与您模拟的通知者程序放在同一个文件中。 否则,您将无法访问该 _$MyNotifier 类。