ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

RIOT 单元测试完全指南:在 tests/unittests 中使用 embUnit 编写、构建与调试嵌入式测试套件

2026/9/20 6:07:43 拓冰建站 浏览量
RIOT 单元测试完全指南:在 tests/unittests 中使用 embUnit 编写、构建与调试嵌入式测试套件 RIOT 单元测试完全指南在 tests/unittests 中使用 embUnit 编写、构建与调试嵌入式测试套件【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOTRIOT 操作系统将所有单元测试集中组织在 tests/unittests 目录下并基于轻量级 C 测试框架embUnit实现。本文以该目录的官方文档为主体结合 tests/unittests/Makefile、tests/unittests/main.c 以及 embUnit 头文件实现如 sys/include/embUnit/AssertImpl.h系统讲解如何构建运行测试、切换输出格式、为任意模块编写测试套件以及如何通过EXTERNAL_UNITTEST_DIRS引入树外测试。读完本文你将掌握 RIOT 单元测试的完整工作流从一条make命令跑通全量测试到为一个新模块按规范添加专属测试集合并接入 CI 自动校验。RIOT 单元测试的组织方式RIOT 采用按模块划分测试目录的组织方式所有测试套件test suite都位于 tests/unittests 下每个被测模块对应一个tests-modulename/子目录。例如tests-core/覆盖core内核模块位运算、链表、消息队列、ringbuffer、XFA 等tests-base64/覆盖base64编解码tests-checksum/覆盖多种校验和实现tests-ztimer/、tests-gnrc_ipv6/、tests-pktbuf/分别覆盖定时器、网络协议栈与包缓冲等子系统。每个tests-modulename/目录通常包含文件作用Makefile将该测试套件编译为独立模块内容通常只有一行include $(RIOTBASE)/Makefile.basetests-modulename.h测试头文件声明该模块全部测试生成函数tests-modulename.c测试实现文件含 fixture 注册tests-modulename-headername.c按被测头文件拆分的测试实现文件可多个Makefile.include可选声明该套件依赖的额外模块如 tests-core/Makefile.include 中的USEMODULE core_mbox从构建系统看tests/unittests/Makefile 通过$(wildcard tests-*/Makefile)自动收集所有测试套件因此新增一个测试套件无需修改任何 Makefile——只要目录名以tests-开头并包含一个 Makefile 就会被自动发现并编译为*.module目标见BASELIBS $(UNIT_TESTS:%%.module)。构建与运行测试构建全部测试在仓库根目录执行cd tests/unittests makemake会调用顶层 tests/unittests/Makefile 中定义的默认构建逻辑把当前目录下所有tests-*/测试套件编译链接成一个可执行镜像。该镜像默认以native平台为目标时可直接在宿主机上运行RIOT 的native目标将嵌入式代码编译为普通 Linux 进程。只构建某个模块的测试当只想验证某个模块时可以指定套件名make tests-coreMakefile会对以tests-开头的 make 目标做特判UNIT_TESTS : $(filter tests-%, $(MAKECMDGOALS))随后仅在搜索目录中定位该套件的 Makefile 并参与构建tests/unittests/Makefile。同理可执行make tests-checksum、make tests-pktbuf等任意套件。运行测试构建完成后运行make termterm是 RIOT 的标准目标它会打开终端连接到串口native平台下直接运行编译出的进程执行测试并打印 embUnit 的统计结果。如果你使用的是真实开发板也可以像烧录任何 RIOT 应用一样把测试镜像烧录到板子上运行参见 tests/unittests/Makefile.ci 中按板卡组织的 CI 配置。调试测试测试也支持使用调试器make debug随后像往常一样使用 GDB 调试。RIOT 的native平台会在调试时加载符号文件对真实板卡则通过 OpenOCD/JLink 等调试器接入。自动化测试与结果校验RIOT 的 CI 与本地测试均通过 tests/unittests/tests/01-run.py 驱动import sys from testrunner import run_check_unittests TIMEOUT 120 if __name__ __main__: sys.exit(run_check_unittests(timeoutTIMEOUT))其底层实现在 dist/pythonlibs/testrunner/init.pytestrunner 会从被测程序输出中解析OK (N tests)格式的统计行据此判断测试是否全部通过nb_tests参数还可用于精确断言通过数量。这意味着任何测试套件最终都必须以OK (... tests)汇总输出这也是 embUnit 默认 textui 输出格式的固定尾部。其他输出格式通过 OUTPUT 环境变量切换RIOT 将 embUnit 的多种输出器Outputter接入到了构建系统设置环境变量OUTPUT即可切换测试结果的呈现方式。五种取值如下OUTPUT取值说明COMPILER编译器风格仅在有测试失败时输出便于与编译器错误信息混排TEXT纯文本逐条列出测试名与结果XMLXML 格式适合交给 CI / 测试框架做结构化解析COLOR默认格式的彩色版本成功/失败分别以绿/红显示COLORTEXT类似TEXT的彩色版本成功/失败分别以绿/红显示该机制在 sys/Makefile.include 中实现当USEMODULE包含embunit时根据OUTPUT的值向编译器注入对应的宏ifeq ($(OUTPUT),XML) CFLAGS -DOUTPUTOUTPUT_XML else ifeq ($(OUTPUT),TEXT) CFLAGS -DOUTPUTOUTPUT_TEXT else ifeq ($(OUTPUT),COMPILER) CFLAGS -DOUTPUTOUTPUT_COMPILER else ifeq ($(OUTPUT),COLORTEXT) CFLAGS -DOUTPUTOUTPUT_COLORTEXT else ifeq ($(OUTPUT),COLOR) CFLAGS -DOUTPUTOUTPUT_COLOR endif运行时tests/unittests/main.c 读取该宏#ifdef OUTPUT TextUIRunner_setOutputter(OUTPUTTER); #endifTextUIRunner_setOutputter()是 sys/embunit/TextUIRunner.c 提供的接口用于在测试运行前替换输出器。仓库中对应实现了 XMLOutputter、TextOutputter、CompilerOutputter、ColorOutputter 与ColorTextOutputter等具体输出器。编译器风格COMPILEROUTPUTCOMPILER make tests-core make term只在测试失败时才产生输出适合嵌入编译脚本快速定位失败点。纯文本风格TEXTOUTPUTTEXT make tests-core make term输出形如- core_bitarithm_tests 1) OK test_SETBIT_null_null 2) OK test_SETBIT_null_limit 3) ... - core_clist_tests 25) ... - ... OK (... tests)可以看到测试按EMB_UNIT_TESTCALLER注册的顺序编号每条用例以OK标记通过失败则标记为失败并附带消息。XML 风格XMLOUTPUTXML make tests-core make term输出形如?xml version1.0 encodingshift_jis standaloneyes ? TestRun core_bitarithm_tests Test id1 Nametest_SETBIT_null_null/Name /Test Test id2 Nametest_SETBIT_null_limit/Name /Test ... /core_bitarithm_tests core_clist_tests Test id25 Nametest_clist_add_one/Name /Test ... /core_clist_tests Statistics Tests.../Tests /Statistics /TestRunTestRun根节点下按测试套件分组每条用例有全局递增的id与Name末尾的Statistics汇总测试总数可直接接入 Jenkins 等 CI 系统解析。编写单元测试三步为模块添加测试套件RIOT 的单元测试统一基于embUnit框架其头文件被 RIOT 重新组织在 sys/include/embUnit 下。为某个模块编写新测试只需完成三件事创建 Makefile新增tests-modulename/Makefile定义测试头文件新增tests-modulename/tests-modulename.h实现测试为每个定义了函数/宏的被测头文件新增tests-modulename/tests-modulename-headername.c若被测模块只有一个这样的头文件则tests-modulename/tests-modulename.c即可。第一步创建 Makefile每个测试套件的 Makefile 内容固定为include $(RIOTBASE)/Makefile.base这会把测试目录注册为 RIOT 的模块module。实际仓库中的 tests-core/Makefile 正是如此。若测试需要额外依赖可在同目录的Makefile.include中追加例如USEMODULE core_mbox见 tests-core/Makefile.include它会被 tests/unittests/Makefile 的-include自动拉入构建。第二步定义测试头文件测试头文件tests-modulename/tests-modulename.h的推荐结构如下以module与header1.h/header2.h占位/* * SPDX-FileCopyrightText: year author * SPDX-License-Identifier: LGPL-2.1-only */ #pragma once /** * addtogroup unittests * { * * file * brief Unittests for the module module * * author author */ #include embUnit/embUnit.h #ifdef __cplusplus extern C { #endif /** * brief Generates tests for header1.h * * return embUnit tests if successful, NULL if not. */ Test *tests_module_header1_tests(void); /** * brief Generates tests for header2.h * * return embUnit tests if successful, NULL if not. */ Test *tests_module_header2_tests(void); /* ... */ #ifdef __cplusplus } #endif /** } */真实的示例参见 tests-core/tests-core.h它为atomic.h、bitarithm.h、cib.h、clist.h、list.h、mbox.h、priority_queue.h、byteorder.h、ringbuffer.h、xfa.h及宏定义分别声明了Test *tests_core_header_tests(void);并额外声明了一个总入口void tests_core(void);用于聚合所有子套件。第三步实现测试每个tests-modulename/tests-module*.c文件的推荐结构如下/* * SPDX-FileCopyrightText: year author * SPDX-License-Identifier: LGPL-2.1-only */ /* clib includes */ #include embUnit.h #include header.h #include tests-module.h /* your macros */ /* your global variables */ static void set_up(void) { /* omit if not needed */ } static void tear_down(void) { /* omit if not needed */ } static void test_function1_what1(void) { /* ... */ TEST_ASSERT(/* ... */); } static void test_function1_what2(void) { /* ... */ TEST_ASSERT(/* ... */); } /* ... */ static void test_function2_what1(void) { /* ... */ TEST_ASSERT(/* ... */); } static void test_function2_what2(void) { /* ... */ TEST_ASSERT(/* ... */); } /* ... */ Test *tests_module_header_tests(void) { EMB_UNIT_TESTFIXTURES(fixtures) { new_TestFixture(test_function1_what1), new_TestFixture(test_function1_what2), new_TestFixture(test_function2_what1), new_TestFixture(test_function2_what2), /* ... */ }; EMB_UNIT_TESTCALLER(module_header_tests, set_up, tear_down, fixtures); /* set up and tear down function can be NULL if omitted */ return (Test *)module_header_tests; }关键点拆解每个test_*函数用TEST_ASSERT*系列宏校验被测函数的行为断言失败会通过 sys/include/embUnit/AssertImpl.h 中的addFailure()记录错误并立即return结束当前用例EMB_UNIT_TESTFIXTURES与EMB_UNIT_TESTCALLER是 embUnit 提供的注册宏定义于 sys/include/embUnit/HelperMacro.h前者声明static const TestFixture fixtures[]数组后者生成static const TestCaller并同时传入set_up/tear_down钩子二者均可传NULL表示不需要命名约定tests_module_header_tests()必须与测试头文件中的声明完全一致否则链接期会报未定义符号。以 tests-core 为例的完整链路tests-core/tests-core-bitarithm.c 是上述模板的典型落地。它对bitarithm.h中的SETBIT/CLRBIT宏以及bitarithm_msb()、bitarithm_lsb()、bitarithm_bits_set()等函数编写了覆盖边界值0x00、UINT_MAX与随机值的用例例如static void test_SETBIT_null_one(void) { unsigned int res 0x00; SETBIT(res, 0x01); TEST_ASSERT_EQUAL_INT(0x01, res); }随后在文件末尾用EMB_UNIT_TESTFIXTURES注册全部 28 个用例并用EMB_UNIT_TESTCALLER(core_bitarithm_tests, NULL, NULL, fixtures)组装套件tests-core-bitarithm.c。而聚合入口tests_core()则通过TESTS_RUN(...)依次运行所有子套件见 tests-core.cvoid tests_core(void) { TESTS_RUN(tests_core_atomic_tests()); TESTS_RUN(tests_core_bitarithm_tests()); TESTS_RUN(tests_core_cib_tests()); ... }main.c中的RUN_TEST_SUITES宏配合 tests/unittests/map.h 中基于 C99 变参宏的MAP()宏会把构建期注入的TEST_SUITES宏展开逐个调用tests_suite()聚合函数最终由TESTS_END()汇总成败并作为进程退出码返回tests/unittests/main.c。embUnit 可用断言宏一览下表为编写用例时可直接使用的断言宏实现细节见 sys/include/embUnit/AssertImpl.h断言说明TEST_ASSERT_EQUAL_STRING(expected, actual)断言字符串actual与expected相等内部通过stdimpl_strcmp比较不等则记录失败TEST_ASSERT_EQUAL_INT(expected, actual)断言整数actual与expected相等内部统一提升为long long比较TEST_ASSERT_NULL(pointer)断言pointer NULLTEST_ASSERT_NOT_NULL(pointer)断言pointer ! NULLTEST_ASSERT_MESSAGE(condition, message)断言condition为真非零失败时输出自定义messageTEST_ASSERT(condition)断言condition为真非零失败时以条件表达式本身作为消息TEST_FAIL(message)直接登记一条失败消息并结束当前用例不执行任何逻辑测试从源码实现可以确认这些宏的语义TEST_ASSERT_EQUAL_INT将两个操作数分别强转为long long后比较TEST_ASSERT_MESSAGE失败时调用TEST_FAIL而TEST_FAIL调用addFailure((message), __LINE__, __FILE__)并return——因此断言失败会立即中止当前用例但不会中断整个套件的其余用例。树外单元测试EXTERNAL_UNITTEST_DIRSRIOT 支持把仓库之外的测试套件一并纳入tests/unittests的构建导出环境变量EXTERNAL_UNITTEST_DIRS其值为空格分隔的目录列表每个目录下需要有遵循相同命名约定的tests-name子目录。这些树外测试与仓库内测试完全等同对待同样被Makefile的UNIT_TEST_SEARCH_DIRS : $(CURDIR) $(EXTERNAL_UNITTEST_DIRS)收集tests/unittests/Makefile同样要求目录内包含tests-name/形式的测试套件内含 Makefile、头文件与实现文件。该特性与EXTERNAL_MODULE_DIRS配合使用效果最佳后者声明包含被测代码的树外模块目录而前者声明对应的树外测试目录二者共同把外部模块 外部测试完整接入 RIOT 构建系统。仓库内的验证示例位于 tests/build_system/external_unittests该应用通过 Makefile 设置EXTERNAL_UNITTEST_DIRS : $(CURDIR)/external_tests_dir引入树外套件tests-out_of_tree并复用tests/unittests的构建逻辑include ../../unittests/Makefile。其中 tests-out_of_tree.c 内部还引用树内测试定义的符号internal_test_was_linked_in从而保证树内 树外测试必须同时被链接这一构建行为可被链接器强制校验。由于该用例主要验证构建系统而非被测代码官方将其限制在native32/native64平台运行。单元测试构建系统的进阶细节关闭自动初始化测试镜像通过DISABLE_MODULE auto_init auto_init_%禁用auto_init由 tests/unittests/main.c 按需手动调用ztimer_init()、ztimer64_init()、xtimer_init()例如依赖定时器的套件需要先行初始化。栈与退出码构建系统统一注入-DTHREAD_STACKSIZE_MAINTHREAD_STACKSIZE_LARGE以保证主线程有足够栈空间并通过-DCONFIG_CORE_EXIT_WITH_MAIN1让进程在main返回后正常退出退出码即测试结果。ASan 支持native32/native64平台默认启用 Address SanitizerASAN_BOARDS ? native32 native64见 tests/unittests/Makefile可在宿主机上捕获越界访问、内存泄漏等错误LLVM 工具链下这两个平台会被加入 CI 黑名单以规避浮点异常误报。串口特性过滤需要auto_init初始化 USB CDC ACM 串口的板卡highlevel_stdio特性被加入FEATURES_BLACKLIST保证测试结果只经标准输出通道返回。目录信息查询make info-unittests可打印当前识别到的全部测试套件名便于排查套件是否被构建系统正确收集。总结RIOT 的单元测试体系以 tests/unittests 为唯一入口依托 embUnit 框架与 Makefile 的自动收集机制做到了按模块组织、按套件构建、多格式输出、可树外扩展。无论是为内核核心模块tests-core还是网络协议栈tests-gnrc_*编写回归用例你只需遵循Makefile 测试头文件 实现文件三步结构配合EMB_UNIT_TESTCALLER注册与TEST_ASSERT*断言即可无缝接入make/make term/make debug的标准开发流程并通过OUTPUT环境变量把结果输出为 CI 友好的文本或 XML 格式。【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOT创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考