10、使用CMocka进行Mock测试:expect_*与will_return_*系列函数实战
单元测试做到一定程度,你一定会遇到一个坎——被测函数调用了外部依赖。比如调了数据库、网络接口、硬件寄存器。这时候你没法真的去连数据库,也不能让硬件真的跑起来。怎么办?Mock。
CMocka 的 Mock 机制,说白了就是「狸猫换太子」。你把真实函数替换掉,让测试代码来控制它的行为。今天我就带你手撕 expect_* 和 will_return_* 这两组核心 API。
Mock 的核心思想:先期望,后返回
CMocka 的 Mock 流程其实就两步:
- 设置期望:告诉框架「等下会有人调用这个函数,参数应该是啥」
- 设定返回值:当调用发生时,返回你指定的值
这两步分别由 expect_* 和 will_return_* 完成。我习惯把它们叫做「先下订单,后收货」。
关键原则:expect_* 必须在 will_return_* 之前调用。顺序反了,测试会直接挂掉。
实战场景:一个温度传感器驱动
假设我们有个底层函数 read_sensor(),它通过 I2C 读取硬件寄存器。上层函数 get_temperature() 调用它,然后做单位转换。
// sensor.h
int read_sensor(uint8_t reg_addr);
float get_temperature(void);
// sensor.c
float get_temperature(void) {
int raw = read_sensor(0x10);
if (raw < 0) {
return -1.0f; // 错误
}
return (float)raw * 0.125f;
}
我们要测 get_temperature(),但不能真的去读硬件。所以 Mock 掉 read_sensor()。
第一步:编写 Mock 函数
// test_sensor.c
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
// Mock 函数
int __wrap_read_sensor(uint8_t reg_addr) {
// 检查参数是否符合预期
check_expected(reg_addr);
// 返回预设的值
return (int)mock();
}
这里有两个关键点:
__wrap_前缀:CMocka 通过链接器把原函数替换成这个check_expected():验证实际传入的参数mock():从队列中取出预设的返回值
我的习惯:Mock 函数名统一用 __wrap_原函数名,这样链接时自动替换,不用改源码。
第二步:编写测试用例
static void test_get_temperature_normal(void **state) {
// 设置期望:参数应该是 0x10
expect_value(__wrap_read_sensor, reg_addr, 0x10);
// 设定返回值:模拟传感器返回 80
will_return(__wrap_read_sensor, 80);
float temp = get_temperature();
assert_float_equal(temp, 10.0f, 0.001f); // 80 * 0.125 = 10.0
}
static void test_get_temperature_error(void **state) {
expect_value(__wrap_read_sensor, reg_addr, 0x10);
will_return(__wrap_read_sensor, -1); // 模拟读取失败
float temp = get_temperature();
assert_float_equal(temp, -1.0f, 0.001f);
}
你看,测试代码非常干净。没有硬件依赖,没有全局变量,所有行为都在测试用例里控制。
expect_* 系列函数详解
CMocka 提供了多种期望检查函数,我列个表方便你查阅:
| 函数 | 作用 | 使用场景 |
|---|---|---|
expect_value(call, param, value) |
检查参数是否等于指定值 | 参数是固定值 |
expect_in_range(call, param, min, max) |
检查参数是否在范围内 | 参数是连续值,比如时间戳 |
expect_not_value(call, param, value) |
检查参数不等于指定值 | 排除某个非法值 |
expect_any(call, param) |
不检查参数,接受任何值 | 参数不重要时 |
expect_string(call, param, str) |
检查字符串参数是否匹配 | 处理字符串参数 |
expect_memory(call, param, data, size) |
检查内存内容是否匹配 | 缓冲区、结构体参数 |
我曾经踩过的坑:expect_string 比较的是整个字符串,不是前缀。如果你只想检查开头几个字符,得自己写辅助函数。
will_return_* 系列函数详解
返回值设定也有多种方式:
| 函数 | 作用 | 说明 |
|---|---|---|
will_return(call, value) |
设定一次返回值 | 最常用,每次调用消耗一个值 |
will_return_count(call, value, count) |
设定多次返回值 | 同一个值返回 count 次 |
will_return_always(call, value) |
设定永久返回值 | 后续所有调用都返回这个值 |
举个例子,如果函数被循环调用三次:
// 第一次返回 10,第二次返回 20,第三次返回 30
will_return(__wrap_read_sensor, 10);
will_return(__wrap_read_sensor, 20);
will_return(__wrap_read_sensor, 30);
// 或者:每次都返回 100
will_return_always(__wrap_read_sensor, 100);
多参数 Mock 实战
真实场景中,函数往往有多个参数。比如一个写寄存器的函数:
int write_register(uint8_t addr, uint8_t value);
// Mock 函数
int __wrap_write_register(uint8_t addr, uint8_t value) {
check_expected(addr);
check_expected(value);
return (int)mock();
}
// 测试用例
static void test_write_ok(void **state) {
expect_value(__wrap_write_register, addr, 0x10);
expect_value(__wrap_write_register, value, 0xFF);
will_return(__wrap_write_register, 0); // 成功
int ret = write_register(0x10, 0xFF);
assert_int_equal(ret, 0);
}
嗯,这里要注意:check_expected 的调用顺序必须和 expect_value 的设置顺序一致。否则参数检查会错位。
SVG:Mock 调用流程
常见陷阱与避坑指南
陷阱一:期望和返回值数量不匹配
如果设置了 3 个 expect_value,但只调用了 2 次 Mock 函数,CMocka 会报错。反过来,如果 Mock 函数被调用了 3 次,但你只设了 2 个 will_return,也会挂。
我的做法:每个测试用例只测一个场景。如果函数被循环调用,用 will_return_count 或 will_return_always 来简化。
陷阱二:参数检查顺序
如果一个 Mock 函数有多个参数,check_expected 的顺序必须和 expect_value 的顺序一致。我曾经在这个问题上 debug 了半小时,最后发现是顺序反了。
陷阱三:指针参数
如果参数是指针,用 expect_memory 而不是 expect_value。因为 expect_value 比较的是指针地址,不是内容。
// 错误:比较的是地址
expect_value(__wrap_write_buffer, buf, my_buffer);
// 正确:比较的是内容
expect_memory(__wrap_write_buffer, buf, my_buffer, sizeof(my_buffer));
总结一下
expect_* 和 will_return_* 是 CMocka Mock 测试的基石。你只需要记住:
- 先设期望,再设返回值
- 期望和返回值的数量要匹配
- 多参数时注意顺序
- 指针参数用
expect_memory
掌握了这些,你就能轻松应对大部分需要 Mock 的场景。下次遇到外部依赖,别怕,Mock 它!
公众号:蓝海资料掘金营,微信deep3321