測試你的提供者程式
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 測試的主要區別在於,
我們必須新增一個 ProviderScope
在 tester.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',
);
});
}
模擬提供者程式
到目前為止,我們已經瞭解瞭如何設定測試以及與提供者程式的基本互動。 但是,在某些情況下,我們可能想要模擬一個提供者程式。
很酷的部分:預設情況下可以模擬所有提供者程式,無需任何額外設定。
這可以透過在 ProviderScope
或 ProviderContainer
上指定 overrides
引數來實現。
請考慮以下提供者程式:
// An eagerly initialized provider.
Future<String> example(Ref 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');
},
);
然後,您可以將其與 mockito
或 mocktail 等包結合使用,以使用它們的 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
類。