9、模拟(Mock)对象基础:什么是Mock、为什么需要Mock、手动实现简单的Mock
好,咱们今天聊点实在的——Mock。
我记得刚入行那会儿,写了个串口驱动模块,自测跑得飞起。结果一集成到主工程,整个系统直接崩了。查了两天,发现是底层硬件寄存器没初始化。那时候我就想:要是能有个东西,让我在测试时“假装”硬件已经就绪,该多好?
嗯,这就是Mock的起源。
什么是Mock?
说白了,Mock就是一个“替身”。
在单元测试里,我们测试的是某个函数或模块。但这个模块往往会调用别的模块——比如调用硬件接口、调用数据库、调用网络服务。这些外部依赖,在测试环境里要么没有,要么不稳定,要么跑起来太慢。
Mock就是造一个假的实现,替换掉这些外部依赖。
举个例子:
// 真实硬件接口
int read_temperature_sensor(void) {
// 操作I2C寄存器,读取温度
return i2c_read(0x48, 0x00);
}
// Mock版本
int mock_read_temperature_sensor(void) {
return 25; // 固定返回25度
}
你看,测试时把真实函数替换成Mock,就能控制返回值了。这样你测试上层逻辑时,就不用关心硬件到底有没有接好。
核心定义:Mock对象是测试替身的一种,它模拟真实对象的行为,并允许你验证交互是否正确。
为什么需要Mock?
我在项目中遇到过好几次这样的场景:
- 依赖还没写好——团队并行开发,你负责模块A,但模块B还没写完。没有Mock,你只能干等。
- 环境不稳定——测试要连数据库,但数据库经常挂。用Mock后,测试再也不依赖数据库了。
- 执行太慢——每次测试都要写文件、读文件,一个用例跑3秒。用Mock后,3毫秒搞定。
- 难以构造异常场景——你想测试“传感器断线”的情况,但真实传感器很难断线。用Mock,直接返回一个错误码就行。
你想想看,如果没有Mock,你的单元测试会变成什么?
它会变成集成测试。每次跑测试都要启动整个系统,部署环境,配置网络……那还叫“单元”测试吗?
我的经验:一个健康的单元测试,应该能在5秒内跑完所有用例。如果超过这个时间,八成是没用好Mock。
手动实现简单的Mock
很多同学一上来就上Mock框架,比如CMock、Unity Mock。但我建议你先手写几次Mock,理解它的本质。
为什么?因为框架只是工具,原理才是根本。你懂了原理,遇到框架解决不了的问题,还能自己造轮子。
场景:温度报警系统
假设我们有这样一个模块:
// temperature_alarm.h
int check_temperature(void); // 返回0表示正常,1表示报警
// temperature_alarm.c
#include "temperature_alarm.h"
#include "sensor.h" // 外部依赖
int check_temperature(void) {
int temp = read_temperature_sensor();
if (temp > 50) {
return 1; // 报警
}
return 0;
}
这里的 read_temperature_sensor() 是外部依赖。我们要测试 check_temperature(),就必须Mock掉这个传感器函数。
第一步:让代码可测试
很多C代码写死了函数调用,没法替换。我们需要加一层“间接层”。
// sensor.h
typedef int (*sensor_read_func_t)(void);
// 默认使用真实传感器
int real_read_temperature(void);
// 允许外部设置Mock函数
void set_sensor_read_func(sensor_read_func_t func);
// sensor.c
static sensor_read_func_t s_sensor_func = real_read_temperature;
void set_sensor_read_func(sensor_read_func_t func) {
s_sensor_func = func;
}
int read_temperature_sensor(void) {
return s_sensor_func();
}
嗯,这里要注意:我们用了函数指针。这样测试时就能把真实函数替换成Mock函数。
第二步:编写Mock函数
// test_temperature_alarm.c
#include "temperature_alarm.h"
#include "sensor.h"
// Mock函数:固定返回高温
int mock_read_high_temp(void) {
return 60;
}
// Mock函数:固定返回正常温度
int mock_read_normal_temp(void) {
return 25;
}
void test_check_temperature_should_alarm_when_high(void) {
set_sensor_read_func(mock_read_high_temp);
int result = check_temperature();
// 断言:应该报警
assert(result == 1);
}
void test_check_temperature_should_normal_when_ok(void) {
set_sensor_read_func(mock_read_normal_temp);
int result = check_temperature();
// 断言:应该正常
assert(result == 0);
}
你看,就这么简单。没有框架,没有宏,就是纯C代码。
避坑指南:我曾经犯过一个错误——在Mock函数里忘了重置状态。比如连续调用两次Mock,第二次返回了错误的值。后来我养成了习惯:每个测试用例开始前,都调用一次 set_sensor_read_func(NULL) 清空状态。
第三步:验证交互
Mock不仅能控制返回值,还能验证“是否被调用过”、“调用了多少次”。
// 带计数功能的Mock
static int g_mock_call_count = 0;
int mock_read_with_count(void) {
g_mock_call_count++;
return 25;
}
void test_check_temperature_should_call_sensor_once(void) {
g_mock_call_count = 0;
set_sensor_read_func(mock_read_with_count);
check_temperature();
// 验证:传感器只被调用了一次
assert(g_mock_call_count == 1);
}
这就是Mock的另一个重要能力——行为验证。不光看结果对不对,还要看过程对不对。
Mock的核心要素
总结一下,手动实现Mock需要三个东西:
| 要素 | 说明 | 代码示例 |
|---|---|---|
| 可替换性 | 代码必须支持替换依赖 | 函数指针、弱符号、条件编译 |
| 可控返回值 | Mock函数能返回预设值 | 固定返回值、查表返回 |
| 可验证交互 | 能检查调用次数、参数 | 全局计数器、参数记录 |
我的建议:刚开始做单元测试时,先用手动Mock。等你写了几十个Mock函数,觉得重复劳动太多时,再上框架。那时候你才知道框架帮你省了什么活。
知识体系图
下面这张图,帮你理清Mock在整个测试体系中的位置:
从图上你能看到:Mock不是唯一的测试替身。Stub只负责返回固定值,Fake是轻量级实现,Spy记录调用信息。而Mock,既要控制返回值,又要验证交互行为。
这也是为什么我说“手动实现一次Mock”很重要——你亲手写了计数、验证逻辑,才能真正理解Mock和Stub的区别。
一个小技巧:如果你发现某个Mock函数越写越复杂,比如要处理各种参数组合、返回不同值,那说明你的被测模块设计得不够好。这时候该考虑重构了,而不是继续堆Mock代码。
好了,关于Mock的基础就聊到这儿。记住一句话:Mock不是目的,可测试性才是。你写Mock,是为了让代码更容易被测试,而不是为了Mock而Mock。
公众号:蓝海资料掘金营,微信deep3321