异步代码测试
在 Dart 中测试异步代码主要处理两种模式:Future(单次异步回调)和 Stream(持续的数据流)。
Dart 的 test 库提供了 expectLater 和专门的异步匹配器(如 completion 和 emits),让异步断言像同步代码一样自然。
测试 Future
如下示例演示了测试 Future的两种主流方式。
// a_futrue_test.dart
import 'package:test/test.dart';
import 'dart:async';
void main() {
group('Future 测试演示', () {
test('方式 A:使用 await 配合 expect (简单直观)', () async {
final result = await fetchUsername();
expect(result, equals('Strawberry'));
});
test('方式 B:使用 expectLater 与 completion (更具声明性)', () async {
// completion 匹配器会等待 Future 完成并校验其结果
await expectLater(fetchUsername(), completion(startsWith('Straw')));
});
test('异步异常校验', () async {
Future<void> failTask() async => throw Exception('Connection Error');
// 使用 throwsA 捕获异步抛出的异常
await expectLater(failTask(), throwsException);
});
});
}
// 模拟一个异步 API 请求
Future<String> fetchUsername() async {
await Future.delayed(Duration(milliseconds: 100));
return 'Strawberry';
}
测试 Stream
测试 Stream 时,通常需要校验流发出的一系列值,这就要用到 emits 系列匹配器。
// a_stram_test.dart
import 'package:test/test.dart';
void main() {
group('Stream 测试演示', () {
test('校验流发出的多个值', () async {
final stream = countStream(3);
// expectLater 会按顺序校验流中的事件
await expectLater(
stream,
emitsInOrder([
1, // 第一个值
2, // 第二个值
3, // 第三个值
emitsDone, // 校验流已正常关闭
]),
);
});
test('校验流中的部分特征', () async {
final stream = countStream(5);
expect(stream, emitsAnyOf([emits(1), contains(5)]));
});
});
}
// 模拟一个计数器流
Stream<int> countStream(int max) async* {
for (int i = 1; i <= max; i++) {
yield i;
}
}
异步匹配器一览
| 匹配器 | 用途 |
|---|---|
completion(m) | 等待 Future 完成,并对结果应用匹配器 m |
emits(v) 断言 Stream | 发出的下一个值是 v |
emitsInOrder([...]) | 按顺序断言 Stream 发出的一系列值 |
emitsDone | 断言 Stream 已经结束,没有更多事件 |
emitsError(m) | 断言 Stream 会发出一个符合 m 的错误事件 |
FakeAsync
FakeAsync 允许你通过“虚构时间”来瞬间完成原本需要等待数秒甚至数小时的任务(如 Future.delayed 或 Timer)。
这里需要引入 fake_async 包。
dart pub add dev:fake_async
下面的例子演示了如何使用FaskAsync。
// a_fake_test.dart
import 'package:test/test.dart';
import 'package:fake_async/fake_async.dart';
void main() {
test('使用 fakeAsync 瞬间完成 1 分钟的等待', () {
fakeAsync((async) {
bool isTriggered = false;
// 1. 设置一个长时间的异步任务
Future.delayed(Duration(minutes: 1), () {
isTriggered = true;
});
// 2. 此时任务还没执行
expect(isTriggered, isFalse);
// 3. 关键操作:拨动时钟,快进 1 分钟
async.elapse(Duration(minutes: 1));
// 4. 现在任务已经执行了!
expect(isTriggered, isTrue);
});
});
}
fakeAsync 创建了一个“时间沙盒”。在这个沙盒里,Dart 的事件循环不再依赖系统时钟,而是由 async.elapse() 手动驱动。只有在 fakeAsync((async) { ... }) 闭包内部启动的异步任务(Future, Timer, Stream)会被受控。在 fakeAsync 内部不要使用 await Future.delayed(这会导致微任务挂起),而应该使用 async.elapse()。如果你只想运行挂起的异步微任务而不推进时钟,可以使用 async.flushMicrotasks()。