ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

嵌入式C单元测试实践:Ceedling安装与第一个测试用例详解

嵌入式C单元测试实践:Ceedling安装与第一个测试用例详解 做嵌入式C开发的朋友应该都有过这种经历想给一个函数补单元测试结果发现得先自己手写一个main函数、手动管理被测模块和一堆依赖文件的编译顺序、还要处理各种头文件路径测试代码没写几行构建脚本倒是堆了一大坨。我最早接触C语言单元测试就是这个状态直到有同事把ceedling甩给我才算把整套流程彻底理顺。这篇先记录从安装到跑通第一个测试用例的完整过程包括中途踩过的坑和规避方法。Ceedling是基于Ruby的一套C语言单元测试构建系统它把Unity断言框架、CMock自动生成Mock、CException异常辅助整合成一个命令行工具本质上是帮你把测试相关的编译、链接、运行、报告全部自动化。对于做嵌入式C开发、想尝试Test-Driven Development或者面对一堆遗留C代码不知道从哪里开始补测试的人这个工具能把大量重复劳动变成一条命令的事。1. 先别急着装Ceedling为什么能省掉那些重复劳动1.1 C语言单元测试到底麻烦在哪里很多人写C代码的时候不做单元测试不是因为不想做而是成本实在太高。一个最简单的被测函数可能依赖好几个全局变量、静态回调、外设寄存器你想把它单独编译成一个测试程序通常得经历这么几步给所有相关模块写stub或者桩函数、手工准备头文件搜索路径、写一个带main函数的测试入口、然后在里面逐个调用断言宏做检查。更麻烦的是工程一大了模块之间的依赖关系会让人疯掉今天加了两个源文件明天测试构建就断给你看。而Ceedling解决的核心问题就是你只需要写测试用例本身剩下的事它全包了。它内置了一套任务引擎会扫描你的源码目录和测试目录自动确定哪些被测模块参与了哪些测试文件的编译自动生成测试的runner文件也就是包含main函数和所有SetUp、TearDown调用的那个文件再调用编译器完成构建最后直接运行测试程序并输出结果。1.2 Unity、CMock、CException三件套的关系Ceedling本身不是一个测试框架它是测试框架的管理器。它默认集成了三样东西Unity一个纯C实现、轻量级的断言框架提供TEST_ASSERT_EQUAL_INT、TEST_ASSERT_EQUAL_STRING这类断言宏以及统一的测试用例注册机制。CMock一个自动生成Mock代码的模块可以根据头文件自动生成函数的桩实现、调用次数校验、参数校验代码。CException一套C语言的异常处理辅助库配合Unity做错误传播处理。这篇是上篇重点先讲Ceedling的安装、工程创建和Unity基础用法CMock的部分放到下篇再展开。2. 装之前先搞定的Ruby环境这里最容易翻车2.1 为什么必须有RubyCeedling是用Ruby写的它的构建流程底层走的是Rake任务系统。这个问题我当初装的时候没想明白结果在纠结窗口报错的时候才反应过来其实Ceedling本质就是一个Ruby的Gem包。所以安装Ceedling之前系统里必须先有Ruby并且Ruby的要能正常执行gem命令。注意这里说的是Ceedling本身需要Ruby和你写的C代码用什么编译器没关系gcc该用还是用gcc。2.2 各平台的安装方式我自己主要用macOS和LinuxWindows环境也帮朋友配过一次按平台分别说Windows建议直接用RubyInstaller下载安装包的时候记得选带DevKit的版本因为后面本地编译某些依赖gem的时候需要DevKit里的工具链。安装过程中有一项Add Ruby executables to your PATH一定要勾上不然后面命令行里敲不了ruby命令。macOS系统自带的Ruby版本比较旧Ceedling对老版本兼容性已经越来越差建议用Homebrew装一个新版brew install ruby。装完之后注意看Homebrew提示的PATH配置新版Ruby在/usr/local/opt/ruby/bin下不配置PATH的话系统用的还是老版本。Ubuntu这种Linux发行版直接sudo apt install ruby-full就行。如果发行版自带的Ruby版本太老建议用rbenv管理一个较新的版本。2.3 装完Ruby先验证这三样安装完成之后依次执行ruby -v gem -v看到版本号输出基本就算成功了。一个容易忽略的点是Gem包管理器有时候和Ruby不是同一个路径如果gem -v报错多半是PATH没配好。另外Ceedling的安装和运行还会用到rake这条命令通常在安装Ceedling时会作为依赖自动装上不用单独操作但如果你在安装前想知道系统里有没有rake可以敲rake --version看一下。3. 正式安装与验证一条gem命令背后的门道3.1 gem install ceedlingRuby环境就绪之后安装本身其实就一条命令gem install ceedling它会自动把Ceedling以及它的依赖包一起装上。依赖里除了rake还有constructor、thor、colorize这类Ruby库。整个过程正常情况下一两分钟就能完成。这里提一个国内网络环境下很常见的问题gem下载源如果速度很慢或者超时可以先把默认源换成本地可访问的镜像源常见的做法是gem sources --remove https://rubygems.org/ gem sources --add https://gems.ruby-china.com/ gem sources -l换完源重新执行install命令速度差距非常明显。这里多说一句任何时候换源都要确认换的是正规的服务安装包来源必须可信别为了图快随便找个不认识的源。3.2 验证安装结果装完之后执行ceedling version如果能输出版本号说明核心安装成功了。这个命令不只是打印版本它还会顺带检查编译器的可用性因为Ceedling运行的时候要调用gcc或者你配置的其他编译器。如果你机器上连编译器都没有比如只装了Ruby就贸然运行这里就会暴露问题。看到版本号之后我建议再补一个空目录测试确认命令行调用没有问题cd /tmp mkdir ceedling_smoke_test cd ceedling_smoke_test ceedling new smoke这条命令如果能正常创建工程目录那安装环节基本就结束了。4. 创建工程之后先把这个目录结构看懂4.1 ceedling new 生成了什么在项目目录下执行ceedling new hello_ceedling命令执行完后当前目录下会多出一个hello_ceedling文件夹里面是这个样子hello_ceedling/ ├── project.yml ├── src/ │ └── .gitkeep ├── test/ │ └── .gitkeep ├── lib/ ├── support/ └── build/我第一次创建完是有点懵的因为src和test目录里只有一个.gitkeep占位文件看起来空空荡荡。但反过来想这也正是Ceedling的设计哲学所有的依赖、插件、框架文件都不放在你的工程里它们都藏在gem安装目录内部你只负责维护自己的源码和测试文件整个工程非常干净。4.2 每个目录和文件的职责project.yml整个工程的配置文件Ceedling的行为全靠它控制后面单独说。src/放被测源码文件。Ceedling默认会扫描这个目录根据测试文件去匹配需要编译的源文件。test/放测试文件。测试文件命名有严格约定必须以test_开头比如test_addition.c对应被测模块addition.c。lib/放你自己引入的第三方库或者公共依赖源码。如果要测试的代码依赖某些自己写的公共模块可以把它们丢到这里。support/放辅助脚本和自定义构建工具。比如需要一个额外的代码生成脚本就可以放在这里然后在project.yml里引用。build/这个目录是全自动生成的里面是编译中间文件、测试可执行文件、生成的runner文件等。不要手工往里面放东西也不要把它提交到版本控制里建议直接加进.gitignore。很多人习惯把Unity、CMock的文件复制到自己工程里在Ceedling这套体系里完全不需要。Ceedling的测试生成插件会从它自己的安装目录里找Unity然后和你的测试代码一起编译。你只要在project.yml里告诉它用哪个测试插件就行默认配置就是开着的。5. 跑通第一个测试从写被测函数到看到绿点5.1 先写一个被测模块为了演示我在src目录下创建了一个简单的加法和乘法模块。addition.h:#ifndef ADDITION_H #define ADDITION_H int add(int a, int b); int multiply(int a, int b); #endifaddition.c:#include addition.h int add(int a, int b) { return a b; } int multiply(int a, int b) { return a * b; }5.2 写测试文件在test目录下创建一个文件名字必须是test_addition.c这样才能和src/addition.c对应上。#include unity.h #include addition.h void setUp(void) { } void tearDown(void) { } void test_add_with_positive_numbers(void) { TEST_ASSERT_EQUAL_INT(5, add(2, 3)); } void test_multiply_with_zero(void) { TEST_ASSERT_EQUAL_INT(0, multiply(5, 0)); }这里有三个约定必须讲清楚。第一setUp和tearDown是每个测试文件的标配哪怕里面什么都不写也必须存在。Ceedling生成的runner里会直接调用这两个函数如果你偷懒不写链接阶段就会报undefined reference而且报错位置挺隐蔽新手容易一头雾水。第二测试用例函数名必须以test_开头这个前缀是Ceedling识别用例的硬性约定。第三像TEST_ASSERT_EQUAL_INT(5, add(2, 3))这种断言第一个参数是期望值第二个参数是实际值别写反了。写反了测试照样能跑但失败时的报错信息会很迷惑人。5.3 运行测试并看懂输出在工程根目录执行ceedling test:all这个命令会编译整个工程里所有匹配到的测试文件运行它们然后输出结果。第一次跑可能有点慢因为要把Unity源码、runner生成、测试源码都编译一遍。跑完之后的输出大致是这样Tool test_compiler assumed to be gcc. Tool test_linker assumed to be gcc. TEST: test_addition.c ---------------------- - test_add_with_positive_numbers PASS - test_multiply_with_zero PASS ---------------------- 2 Tests 2 Failures 0 Ignored OK如果你只改了某一个测试文件不需要跑全量测试可以直接指定文件名ceedling test:test_addition这里的参数就是测试文件名去掉.c后缀。日常开发建议频繁用这个命令因为它的反馈速度比test:all快很多。5.4 制造一个失败看看报错长什么样一个测试框架靠不靠谱失败时的信息友好度很关键。我把断言故意改错TEST_ASSERT_EQUAL_INT(6, add(2, 3));重新运行输出会明确告诉你test_addition.c:22:test_add_with_positive_numbers:FAIL: Expected 6 Was 5包含文件、行号、用例名、期望值和实际值定位问题非常直接。这个特性在你后面写大批量测试的时候价值巨大。6. project.yml新手只需要动这几个位置6.1 基本配置结构project.yml是YAML格式。Ceedling项目刚生成时它包含了很多项新手看着容易慌但其实绝大多数都是默认值。真正需要你关注的主要是三块:project、:paths、:tools。我看过太多人一上来就改一堆配置结果越改越跑不通其实老老实实用默认配置跑起来后面再按需调整才是不走弯路的方式。一个典型的新手配置长这样:project: :use_exceptions: FALSE :use_test_preprocessor: TRUE :use_auxiliary_dependencies: TRUE :build_root: build :release_build: :enabled: FALSE :paths: :test: - test/** :source: - src/** :support: - support/** :tools: :test_compiler: :executable: gcc :arguments: - -c - ${1} - -I$: COLLECTION_PATHS_TEST_TOOLCHAIN_INCLUDE - -I$: COLLECTION_PATHS_TEST_SUPPORT_SOURCE_INCLUDE_VENDOR - -D$: COLLECTION_DEFINES_TEST_AND_VENDOR - -o ${2} :test_linker: :executable: gcc :arguments: - ${1} - -o ${2} - -I$: COLLECTION_PATHS_TEST_TOOLCHAIN_INCLUDE - -I$: COLLECTION_PATHS_TEST_SUPPORT_SOURCE_INCLUDE_VENDOR - -L$: COLLECTION_PATHS_BUILD_TEST_SUPPORT这些COLLECTION_开头的变量是Ceedling内置的构建路径集合一般不需要手动改。真正需要动手的地方在下面。6.2 新手最常改的四个配置点第一路径扩展。默认的:paths配置是扫描src/**和test/**。如果你的源码放在两个位置比如src/和middleware/可以追加:paths: :source: - src/** - middleware/**第二编译宏定义。嵌入式开发几乎绕不开条件编译比如代码里有#ifdef ENABLE_DEBUG的段。测试时要让某个宏生效需要在:defines里加上:defines: :test: - ENABLE_DEBUG - F_CPU16000000UL第三额外的编译选项。需要给编译器传-Wall、-stdc99这类参数在:tools里的:test_compiler的:arguments中加。第四链接的额外库。被测代码如果依赖某个外部的静态库在:test_linker的:arguments中加入-lm或者-L/path/to/lib -lmylib。这里有一个很重要的方法论每改一个配置就跑一次测试。改坏了能立刻定位到是哪一项导致的而不是一次性改一堆最后分不清是谁的问题。我在新项目中就是这么干的看起来慢实际上省时间。7. 上手阶段我踩过的坑命名、路径与缓存7.1 测试文件命名必须严格遵守约定这是Ceedling新手最容易踩的坑我也在这里栽过。测试文件必须以test_开头比如你要测foo.c测试文件就应该叫test_foo.c。如果你随手起名叫foo_test.c或者fooTest.cCeedling的扫描器根本不会把它识别成测试文件执行ceedling test:all的时候它安安静静躺在那里好像什么都没发生。还有一个容易忽略的细节测试文件和被测文件如果不在默认的test/和src/目录下记得在project.yml里把路径加进:paths配置不然同样会出现文件存在但测试没跑的诡异情况。7.2 工程路径别带空格和中文这个建议听起来很玄学但我是吃过亏的。Ceedling的底层构建是靠一系列Ruby字符串拼接命令来执行的路径里一旦有空格命令行参数解析就会出各种奇怪的问题有时候报的错根本看不出和空格有关。比如我遇到过编译器找不到头文件的报错排查了半天最后发现是工程路径里有一个空格引起的。所以新工程尽量放在纯英文、无空格的路径下比如~/projects/ceedling_demo。这一点对Windows用户尤其重要因为Windows的用户名目录经常是中文直接把工程放在C:\Users\张三\project下很容易触发问题。实在避不开可以先放在一个干净路径下工作或者想别的办法绕过但不要和路径解析硬刚。7.3 构建缓存失效的经典处理手段Ceedling的build/目录会缓存之前编译的中间产物。日常改源码和测试文件没问题但是改动了project.yml、增删了头文件搜索路径、或者把src目录结构大改之后某些时候可能会出现明明改了代码测试结果还是旧的这种情况。原因就是旧的中间文件没有被正确识别为过期。这时候用两条命令之一强制清理ceedling clean或者更彻底地ceedling clobber两者的区别在于clean会清理测试构建产物而clobber连release构建产物一起清掉。绝大多数情况下clean就够用了。我把这两条命令当成遇到诡异问题的第一反应与其花半个小时猜谜不如十秒钟重建来得干脆。7.4 Unity断言宏别直接塞进if条件里写测试用例多了之后有人习惯把断言写得更紧凑比如if (result ! 0) TEST_ASSERT_EQUAL_INT(1, result);Unity的断言宏内部展开后是if (!condition) { ...fail... }这样的结构它自身就带if控制流。一旦你把断言放在另一个if控制流里宏展开之后就容易出现if ... if ... else ...的else悬空问题结果要么编译报错要么编译过了但逻辑和你想的不一样。更稳的写法是这样TEST_ASSERT_EQUAL_INT(1, result);如果不想让某个失败中断整个测试流程用TEST_ASSERT_EQUAL_INT_MESSAGE(1, result, custom message)尽量不要把断言嵌进条件里。真要做条件判断也要把所有分支用大括号包好这是C语言宏地狱里的基本保命手段。写到这里Ceedling的安装、工程创建、第一个测试用例和基础配置已经全部跑通了。我个人的体会是Ceedling最值钱的地方不是省了那几条编译命令而是它把写测试变成了纯粹的写测试——你只需要专注断言和用例逻辑剩下所有构建细节都被藏起来了。下一篇我会重点拆CMock的用法包括怎么给依赖硬件操作的函数生成Mock、怎么校验Mock函数的调用参数和次数到时候见。
返回列表