异步代码测试

在 Dart 中测试异步代码主要处理两种模式:Future(单次异步回调)和 Stream(持续的数据流)。 Dart 的 test 库提供了 expectLater 和专门的异步匹配器(如 completionemits),让异步断言像同步代码一样自然。

测试 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.delayedTimer)。 这里需要引入 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()

Reference