摘要:本文介绍如何利用 Unity 与 Ceedling 在嵌入式 C 项目中搭建轻量级单元测试框架。文章从环境准备、工程初始化、被测模块编写、测试用例设计,到使用 CMock 隔离外部依赖、接入 CI 持续集成,完整演示了整套落地流程,并总结了头文件路径、Mock 冲突、浮点比较等常见注意事项,帮助开发者在纯主机环境下快速建立可维护、可重复的测试体系。
1. 引言
在嵌入式开发中,C 语言项目的单元测试一直是一个容易被忽视的环节。传统做法往往依赖硬件板卡、手工验证,导致回归成本高、反馈周期长。Ceedling 作为一套基于 Ruby 的构建与测试框架,能够把 Unity 测试框架、CMock 模拟库和 CException 异常处理整合到一起,帮助开发者在纯主机环境下快速搭建轻量级的 C 单元测试工程。本文将以 Unity 与 Ceedling 的组合为主线,从环境搭建、工程结构、用例编写到持续集成,完整演示如何在嵌入式 C 项目中落地一套可维护的单元测试体系。
2. 为什么选择 Unity 与 Ceedling
在众多 C 语言测试方案中,Unity 与 Ceedling 的组合之所以受到嵌入式开发者青睐,主要源于以下几点:
- 轻量级:Unity 本身只包含少量源文件,不依赖复杂的运行时环境,非常适合资源受限的嵌入式交叉编译场景。
- 自动化程度高:Ceedling 自动扫描测试文件、生成测试运行器(Runner)、管理模拟(Mock)与桩(Stub),减少手工维护成本。
- 与构建系统解耦:测试工程与目标固件工程相互独立,可在 PC 上快速运行,无需连接开发板。
- 生态成熟:CMock 可自动为指定头文件生成 Mock,CException 则简化了异常路径的测试编写。
对于团队而言,这套方案能够显著缩短反馈回路,让单元测试真正融入日常开发流程。
3. 环境准备与安装
在开始搭建之前,需要先准备好以下基础环境:
- Ruby 2.5 及以上版本(Ceedling 依赖 Ruby 运行环境)。
- GCC 或 Clang 编译器(用于在主机上编译测试代码)。
- Git(用于拉取 Ceedling 及管理测试工程)。
安装 Ceedling 非常简单,在终端中执行以下命令即可:
gem install ceedling安装完成后,可以通过以下命令验证是否成功:
ceedling version如果能够正常输出版本号,说明环境已经就绪。接下来就可以创建第一个测试工程了。
4. 创建工程与目录结构
Ceedling 提供了工程初始化命令,可以快速生成一套标准的目录骨架:
ceedling new unity_demo执行后会自动生成如下目录结构:
unity_demo/ ├── src/ # 被测源码 ├── test/ # 测试用例 ├── lib/ # 第三方库(Unity、CMock 等) ├── build/ # 构建产物 ├── project.yml # 工程配置文件 └── rakefile.rb # Rake 任务入口其中src目录存放被测模块的源文件与头文件,test目录存放对应的测试用例文件,project.yml是 Ceedling 的核心配置文件,用于指定编译器、头文件路径、模拟开关等参数。
5. 编写第一个被测模块
为了演示完整的测试流程,我们先在src目录下创建一个简单的计算器模块。首先是头文件calculator.h:
#ifndef CALCULATOR_H #define CALCULATOR_H int add(int a, int b); int subtract(int a, int b); #endif然后是源文件calculator.c:
#include "calculator.h" int add(int a, int b) { return a + b; } int subtract(int a, int b) { return a - b; }这个模块虽然简单,但足以演示 Ceedling 的测试编写、编译和运行流程。
6. 编写测试用例
在test目录下创建测试文件test_calculator.c。Ceedling 约定测试文件以test_前缀命名,并包含被测模块的头文件:
#include "unity.h" #include "calculator.h" void setUp(void) { } void tearDown(void) { } void test_add_should_return_sum_of_two_numbers(void) { TEST_ASSERT_EQUAL_INT(5, add(2, 3)); TEST_ASSERT_EQUAL_INT(-1, add(2, -3)); } void test_subtract_should_return_difference(void) { TEST_ASSERT_EQUAL_INT(1, subtract(3, 2)); TEST_ASSERT_EQUAL_INT(5, subtract(2, -3)); }其中setUp和tearDown是每个用例执行前后的钩子函数,用于初始化和清理资源。测试函数以test_前缀命名,Ceedling 会自动识别并生成对应的测试运行器。
7. 运行测试
在工程根目录下执行以下命令即可编译并运行全部测试:
ceedling test:all如果一切正常,终端会输出类似下面的结果:
Test 'test_calculator.c' ----------------------- - 4 Tests, 4 Assertions, 0 Failures, 0 Ignored OVERALL: PASS也可以只运行单个测试文件,加快调试速度:
ceedling test:calculator当测试失败时,Ceedling 会明确指出失败的用例、断言位置和期望值与实际值,方便快速定位问题。
8. 使用 CMock 处理依赖
真实项目中,被测模块往往依赖外部硬件驱动或其他模块。CMock 可以自动为指定头文件生成模拟对象,从而隔离被测单元。假设sensor.c依赖adc.h提供的adc_read()函数,我们可以在测试文件中这样使用:
#include "unity.h" #include "mock_adc.h" #include "sensor.h" void test_sensor_read_should_use_adc_value(void) { adc_read_ExpectAndReturn(42); TEST_ASSERT_EQUAL_INT(42, sensor_read()); }在project.yml中需要把adc.h加入 CMock 的处理列表:
:cmock: :treat_externs: :include :includes_h: - adc.h这样 Ceedling 就会在编译测试时自动生成mock_adc.h和mock_adc.c,无需手工维护模拟代码。
9. 集成到持续集成流程
为了让单元测试真正发挥价值,建议把测试命令接入 CI 流水线。以 GitHub Actions 为例,可以在仓库中创建.github/workflows/test.yml:
name: Unit Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Ruby uses: ruby/setup-ruby@v1 with: ruby-version: '3.2' - name: Install Ceedling run: gem install ceedling - name: Run Tests run: ceedling test:all这样每次代码提交后,CI 都会自动拉取代码、安装依赖并运行全部单元测试,一旦出现回归就能第一时间发现。
10. 常见问题与注意事项
在实际使用过程中,有几个容易踩坑的地方值得注意:
- 头文件路径配置:如果被测模块引用了非标准路径下的头文件,需要在
project.yml的:paths:中显式添加搜索路径。 - Mock 与真实实现冲突:当测试文件同时包含真实头文件和 Mock 头文件时,链接阶段可能产生符号冲突,应避免在同一个测试文件中混用。
- 浮点比较:Unity 默认使用精确比较,涉及浮点数时应使用
TEST_ASSERT_FLOAT_WITHIN等带误差范围的断言。 - 交叉编译环境:如果需要在目标架构上运行测试,可以在
project.yml中指定交叉编译器,但通常建议先在主机上完成大部分逻辑测试。
11. 总结
通过 Unity 与 Ceedling 的组合,嵌入式 C 项目可以在不依赖硬件的情况下建立一套轻量、可重复的单元测试体系。Ceedling 自动化的构建与运行流程大幅降低了测试用例的维护成本,而 CMock 则帮助开发者轻松隔离外部依赖。将测试接入 CI 后,团队能够在每次提交时快速获得反馈,从源头减少回归缺陷。希望本文的实战步骤能够帮助你顺利在自己的项目中落地这套测试框架。