依赖模拟 (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>()));
});
}