10、使用CMocka进行Mock测试:expect_*与will_return_*系列函数实战

单元测试做到一定程度,你一定会遇到一个坎——被测函数调用了外部依赖。比如调了数据库、网络接口、硬件寄存器。这时候你没法真的去连数据库,也不能让硬件真的跑起来。怎么办?Mock。

CMocka 的 Mock 机制,说白了就是「狸猫换太子」。你把真实函数替换掉,让测试代码来控制它的行为。今天我就带你手撕 expect_*will_return_* 这两组核心 API。

Mock 的核心思想:先期望,后返回

CMocka 的 Mock 流程其实就两步:

  1. 设置期望:告诉框架「等下会有人调用这个函数,参数应该是啥」
  2. 设定返回值:当调用发生时,返回你指定的值

这两步分别由 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 调用流程

CMocka Mock 调用流程 测试用例 expect_value() will_return() __wrap_read_sensor() 预设返回值 check_expected() mock() 获取返回值 get_temperature() 流程:测试用例先设置期望和返回值 → 被测函数调用 Mock → Mock 检查参数 → 返回预设值 设置阶段 Mock 函数 被测函数

常见陷阱与避坑指南

陷阱一:期望和返回值数量不匹配

如果设置了 3 个 expect_value,但只调用了 2 次 Mock 函数,CMocka 会报错。反过来,如果 Mock 函数被调用了 3 次,但你只设了 2 个 will_return,也会挂。

我的做法:每个测试用例只测一个场景。如果函数被循环调用,用 will_return_countwill_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