依赖模拟 (Mock)

真实的系统充满了各种“麻烦”的依赖(如数据库、网络请求、文件系统、传感器等)。为了保证单元测试的速度、稳定(不因断言时没网而失败)和纯粹(只测当前函数逻辑),我们需要使用 Mock(模拟对象)。

什么是 Mock

Mock 对象就像是真实依赖的一个“替身”。我们可以通过它:

  • Stubbing(存根):预设它的行为(例如:当调用 getData() 时,返回 OK)。
  • Verification(验证):检查它是否被调用(例如:确认 save() 函数确实被执行了一次)。

引入 Mocktail

本文将使用 mocktail 包来模拟外部依赖。 首先向Dart Project添加 dev:mocktail 依赖:

dart pub add dev:mocktail

常用函数

语法说明
when(...).thenReturn(val)针对同步方法,设定返回固定值
when(...).thenAnswer((_) async => val)针对异步方法,设定返回 Future 或 Stream
when(...).thenThrow(error)模拟抛出异常(测试错误处理逻辑)
any()匹配器,表示接收任何参数
verify(...).called(n)检查该方法是否被调用了 n 次
verifyNever(...)确保该方法在测试中从未被调用

Mock 实战1: 模拟数据库服务

假设我们有一个 User 模型和一个负责逻辑处理的 UserService

// >>>>>> 业务代码 <<<<<<

class User {
  final int id;
  final String name;
  User(this.id, this.name);
}

// 数据库接口层
abstract class UserRepository {
  Future<User?> getUserById(int id);
  Future<void> saveUser(User user);
}

// 业务逻辑层 (我们要测试的对象)
class UserService {
  final UserRepository repository;
  UserService(this.repository);

  Future<String> fetchUserName(int id) async {
    final user = await repository.getUserById(id);
    return user == null ? 'Unknown User' : user.name;
  }
}

下面我们模拟 UserRepository 的行为,以测试 UserService 在不同数据库返回情况下的表现。

// mock_db_test.dart
import 'package:test/test.dart';
import 'package:mocktail/mocktail.dart';

// 第一步:创建 Mock 类
class MockUserRepository extends Mock implements UserRepository {}

void main() {
  late MockUserRepository mockRepo;
  late UserService userService;

  setUp(() {
    mockRepo = MockUserRepository();
    userService = UserService(mockRepo);
  });

  group('UserService.fetchUserName 数据库模拟测试', () {
    test('当数据库能找到用户时,返回用户名', () async {
      // 第二步:Stubbing (存根) - 模拟数据库返回一个真实对象
      when(
        () => mockRepo.getUserById(1),
      ).thenAnswer((_) async => User(1, 'Mocktail'));

      final result = await userService.fetchUserName(1);

      expect(result, equals('Mocktail'));
      // 第三步:验证 - 确认业务逻辑确实去查了 ID 为 1 的用户
      verify(() => mockRepo.getUserById(1)).called(1);
    });

    test('当数据库找不到用户时 (返回 null),返回 Unknown User', () async {
      // 模拟数据库返回 null
      when(() => mockRepo.getUserById(any())).thenAnswer((_) async => null);

      final result = await userService.fetchUserName(999);

      expect(result, 'Unknown User');
      verify(() => mockRepo.getUserById(999)).called(1);
    });

    test('当数据库连接超时抛出异常时,业务层应该感知错误', () async {
      // 模拟数据库崩溃
      when(
        () => mockRepo.getUserById(any()),
      ).thenThrow(Exception('Database connection failed'));

      // 验证业务层是否处理或直接抛出了异常
      expect(() => userService.fetchUserName(10), throwsException);
      verify(() => mockRepo.getUserById(10)).called(1);
    });
  });
}

Mock 实战2: 模拟 Stream

在测试使用了响应式编程技术或WebSocket的程序时,时常免不了要模拟 Stream。

模拟一个简单的 Stream

假设你有一个监控系统,它通过 Stream 实时推送传感器数据。

// >>>>>> 业务代码 <<<<<<

abstract class SensorRepository {
  Stream<int> get temperatureStream;
}

下面的示例演示了如何模拟 Stream。

// mock_stream_test.dart
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';

class MockSensorRepository extends Mock implements SensorRepository {}

void main() {
  final mockRepo = MockSensorRepository();

  test('模拟Stream A', () {
    // 方式 A:直接返回一个预定义好的流
    when(
      () => mockRepo.temperatureStream,
    ).thenAnswer((_) => Stream.fromIterable([20, 21, 22]));

    // 断言校验
    expect(mockRepo.temperatureStream, emitsInOrder([20, 21, 22]));
  });

  test('模拟Stream B', () {
    // 方式 B:返回一个带延迟的流(更真实)
    when(() => mockRepo.temperatureStream).thenAnswer(
      (_) => Stream.periodic(
        const Duration(milliseconds: 10),
        (i) => 20 + i,
      ).take(3),
    );

    // 断言校验
    expect(mockRepo.temperatureStream, emitsInOrder([20, 21, 22]));
  });
}

使用 StreamController 动态控制 Stream

有时需要在测试运行过程中,手动触发流的发送。这时可以使用 StreamController

// mock_streamc_test.dart
import 'dart:async';

import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';

class MockSensorRepository extends Mock implements SensorRepository {}

void main() {
  test('通过 StreamController 控制 Stream', () async {
    final mockRepo = MockSensorRepository();
    final controller = StreamController<int>();

    // 将 Mock 的返回指向 Controller 的流
    when(() => mockRepo.temperatureStream).thenAnswer((_) => controller.stream);

    // 此时测试会监听流,但流里还没东西
    final expectation = expectLater(
      mockRepo.temperatureStream,
      emitsInOrder([20, 30]),
    );

    // 手动推送数据
    controller.add(20);
    controller.add(30);

    // 别忘了关闭流
    await controller.close();
    await expectation;
  });
}

模拟流中的错误 (Stream Errors)

下面的例子演示了“测试业务代码如何处理异常流”。

// mock_streame_test.dart
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';

class MockSensorRepository extends Mock implements SensorRepository {}

void main() {
  test('模拟流抛出错误', () {
    final mockRepo = MockSensorRepository();

    when(
      () => mockRepo.temperatureStream,
    ).thenAnswer((_) => Stream<int>.error(Exception('Sensor Malfunction')));

    expect(mockRepo.temperatureStream, emitsError(isA<Exception>()));
  });
}

Reference