跳到主要内容

测试你的提供者程序

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 测试的主要区别在于, 我们必须添加一个 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 interact with your providers
expect(
container.read(provider),
'some value',
);
});
}

模拟提供者程序

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

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

请考虑以下提供者程序:

// An eagerly initialized provider.

Future<String> example(ExampleRef 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');
},
);

然后,您可以将其与 mockitomocktail 等包结合使用,以使用它们的 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 类。