temp_controller在没有真实温度传感器的情况下,通过了主机上的单元测试,能独立运行。测试通过后,输出了用例结果,所有断言都通过,测试环境里没有访问到真实硬件。

回到测试跑通前的最后一步。我们把生成的 Mock 和被测模块一起编译到测试工程里,写了一个名为 test_temp_controller.c 的单元测试文件,里面调用了 temp_controller 的接口,断言返回值和状态。编译通过后在宿主机上执行,用例逐个跑完,输出日志能看见 temp_controller 按预期调用了 mock 的接口,交互顺序和参数都在断言范围内。把测试跑通这件事放在首位,是检验隔离是否到位的最直观方式。
再往前看,是如何把真实依赖替换成假的。temp_controller.c 里有对 temperature_sensor 模块的调用。如果直接去调用板上的传感器,单元测试就无法在宿主机上稳定运行。解决办法是用 CMock 生成一个假的 temperature_sensor,实现与头文件一致的接口,然后在链接时让测试程序调用这个假实现。这样,temp_controller 在测试中就不再依赖外设。

CMock 的工作原理比较直接:它读你的头文件,根据声明自动生成一个 mock 实现和相应的断言/期望接口。底层实现实则是函数调用的“重定向”——当被测代码要跳到原函数时,链接过来的是 mock 的实现。换句话说,CPU 执行被测代码时,指令不再去真实实现那儿,而是跑到我们生成的假函数里。对嵌入式开发来说,这一步很关键,能把硬件依赖从逻辑验证里剥离出来。
CMock 是 ThrowTheSwitch 工具链的一员,和 Unity 集成得比较紧密。源码和文档在
https://github.com/ThrowTheSwitch/CMock ,采用 MIT 许可证。仓库里会包含一些子模块,里面就有 Unity。拿到代码后,执行 git submodule update –init –recursive,就能把 Unity 和其他子模块拉到本地,避免运行时找不到依赖。

项目里需要一个最小的 CMock 配置文件 cmock_config.yml,用来指定生成 mock 的规则和风格。这个文件里你会写要 mock 的头文件列表,例如 temperature_sensor.h,还能控制生成文件的命名、是否生成期望检查函数、是否启用参数比较等。配置好后,运行 CMock 的生成脚本,一键把 mock 文件做好。生成出来的文件一般是 Mocktemperature_sensor.c / Mocktemperature_sensor.h(命名规则受 cmock_config.yml 控制),可以直接加入测试工程。
在把 mock 加入项目之前,要先理清被测模块的接口。temp_controller.h 是被测模块对外的接口头。temp_controller.c 里直接 include temperature_sensor.h 并调用其函数。用 CMock 生成 mock 的步骤就是:把 temperature_sensor.h 放到配置里,运行生成器,得到 Mocktemperature_sensor,实现里有模拟返回值、记录调用参数和带断言的检查函数。然后在测试代码里 include Mocktemperature_sensor.h,按测试用例设定期待和返回值,调用 temp_controller 的函数,最后用 Unity 的断言来验证行为。

实际操作流程可以概括为:把 ThrowTheSwitch 的仓库 clone 下来,初始化子模块(包括 Unity),在工程根目录放一个 cmock_config.yml,指定 temperature_sensor.h,运行 CMock 生成 mock,写 test_temp_controller.c,编译并运行测试。每一步都有细节要注意,列如头文件路径、生成器的版本兼容、链接顺序等。尤其是链接时,要确保 Mock 的符号覆盖了真实模块的符号,否则编译会通过但运行还是去调用真实函数。
说回嵌入式单元测试的常见困境:硬件依赖、驱动耦合、外设初始化复杂、随机性高。框架不是全部,隔离做得不够,单元测试就会很脆弱。CMock 和 Unity 提供的是一种工程化的方法:用头文件驱动 mock 生成,把重复的劳动力交给工具,测试人员把精力放到设计用例和边界条件上。把细节做好了,测试就能在宿主机上稳定重复运行,这对调试逻辑很有协助,也方便 CI 集成。不过实际用起来还是需要动手调一些配置,别以为一键全搞定。

