嵌入式C语言单元测试实战:Unity框架入门与工程实践
1. 项目概述:为什么嵌入式开发也需要单元测试?
在嵌入式开发领域,尤其是使用C语言进行单片机、RTOS或裸机程序开发时,我们常常陷入一种“烧录-看灯-调串口”的循环。代码逻辑稍微复杂一点,比如一个状态机或者一个协议解析器,一旦出了问题,定位起来就非常痛苦。你可能需要反复插拔调试器,在关键位置打上断点,或者通过串口打印一堆日志,效率低下不说,还容易引入新的问题。更头疼的是,当项目需要迭代,或者有新人接手时,如何保证你修改的代码没有破坏原有的功能?这时候,单元测试的价值就凸显出来了。
“嵌入式C单元测试框架unity-初体验”这个标题,指向的就是一个在嵌入式C开发圈子里颇有名气的轻量级测试框架——Unity。它不是什么高深的理论,而是一套实实在在的工具,能让你像写普通C函数一样,为你的模块(比如一个驱动、一个算法函数)编写测试用例,然后一键运行,快速验证其行为是否符合预期。对于习惯了“硬件思维”的嵌入式工程师来说,这相当于给你的软件逻辑上了一道“保险”,让你在代码真正跑在目标板上前,就能在PC上(或模拟器上)发现大部分逻辑错误。
我最初接触Unity也是因为一个血泪教训:一个看似简单的CRC校验函数,在某种边界条件下出了错,导致整个通信链路在特定场景下不稳定,这个问题在集成测试阶段花了将近一周才定位到。如果当时为这个函数写了单元测试,可能半小时就发现问题了。所以,这次“初体验”不仅仅是尝试一个新工具,更是对嵌入式开发流程的一种优化和反思。无论你是刚入行的新手,还是经验丰富的老手,花点时间掌握单元测试,都能让你的代码更健壮,项目交付更安心。
2. Unity框架核心设计与思路拆解
2.1 Unity是什么?为什么选择它?
Unity并不是一个庞大的IDE或者复杂的系统,它本质上就是一组纯C语言编写的头文件(.h)和源文件(.c)。它的设计哲学非常“嵌入式友好”:极简、可移植、零外部依赖。你不需要安装什么复杂的运行时环境,只需要把unity.c和unity.h、unity_internals.h这几个文件拷贝到你的项目里,就能开始写测试了。
在嵌入式C的测试框架领域,除了Unity,你可能还听说过CppUTest、CMocka等。我选择从Unity开始体验,主要基于以下几点考量:
- 极致轻量:Unity的源码就几千行,编译后体积很小,甚至可以把它和你的测试代码一起放到资源受限的单片机上运行(当然,更常见的做法是在PC上运行测试)。这种轻量级特性,非常契合嵌入式开发对资源敏感的特点。
- 学习曲线平缓:它的API设计直观,提供的断言(Assert)宏非常类似其他语言测试框架(如Java的JUnit),对于有编程基础的人来说很容易上手。你不需要先学一套复杂的Mock(模拟)框架才能开始。
- 强大的断言系统:这是测试框架的核心。Unity提供了丰富的断言宏来验证各种条件,比如
TEST_ASSERT_EQUAL_INT(判断整型相等)、TEST_ASSERT_LESS_THAN_FLOAT(判断浮点数小于)、TEST_ASSERT_EQUAL_STRING(判断字符串相等)等等。这些宏在测试失败时会给出清晰的错误信息,包括期望值和实际值,极大方便了问题定位。 - 与CMock天然搭配:Unity来自ThrowTheSwitch组织,同一个组织下还有CMock(用于生成Mock代码)和CException(轻量级异常处理)。这意味着当你后续需要测试那些依赖外部硬件(如I2C、SPI)或复杂模块的函数时,可以平滑地引入CMock来模拟这些依赖,形成一套完整的测试工具链。
简单来说,Unity就像一把专门为C语言打造的“手术刀”,精准、小巧,能帮你快速解剖和验证代码逻辑,而不需要动用“重型机床”。
2.2 嵌入式单元测试的独特挑战与Unity的应对
在PC或服务器上做单元测试相对直接,但在嵌入式环境中,我们会面临一些特殊挑战,Unity的设计也考虑到了这些点:
- 硬件依赖:你的代码里可能充满了
read_gpio()、send_uart()这样的硬件操作函数。直接在目标板上测试,环境难以复现,速度慢。Unity鼓励的是**“宿主测试”** 或“仿真测试”,即在PC上编译和运行测试代码。这就要求我们把硬件相关的代码通过一层抽象(如硬件抽象层HAL)隔离开,在测试时用“桩函数”或后续用CMock生成的Mock函数来替代。Unity本身不解决硬件隔离问题,但它能与这种架构很好地协同工作。 - 编译器与标准库差异:不同的嵌入式编译器(如GCC for ARM, IAR, Keil MDK)对C标准的支持度不同。Unity的代码编写时充分考虑了可移植性,尽量避免使用编译器特有的扩展或行为不确定的标准库函数。对于像
malloc或printf这类可能不可用或行为不一致的函数,Unity允许你自定义实现(通过重定义宏),比如将测试结果输出到串口或者一个内存缓冲区。 - 测试结果输出:在无操作系统的裸机环境,没有
printf到控制台。Unity提供了UNITY_OUTPUT_CHAR宏,你可以把它重定向到任何你想要的输出通道,比如串口、SEGGER RTT、或者只是设置一个标志位。这样你就能在调试器里或者通过日志工具看到测试结果。
理解了这些,我们就明白,使用Unity不仅仅是写几个测试函数,它更推动我们思考如何写出“可测试的”嵌入式代码——即代码结构清晰、模块间耦合度低、硬件依赖被良好封装。这本身就是一个非常好的工程实践。
3. 核心细节解析与实操要点
3.1 Unity工程的文件结构组织
开始写测试前,一个清晰的文件结构能让后续工作事半功倍。我推荐的组织方式如下:
你的项目根目录/ ├── src/ # 你的产品源代码 │ ├── driver/ # 驱动程序 │ ├── module/ # 业务逻辑模块 │ └── hal/ # 硬件抽象层 ├── test/ # 测试专用目录 │ ├── unity/ # 放置Unity框架源码(unity.c, unity.h, unity_internals.h) │ ├── src/ # 针对产品源码的测试文件 │ │ ├── test_module_a.c # 对module_a.c的测试 │ │ └── test_driver_b.c # 对driver_b.c的测试 │ ├── runner/ # 测试运行器(Test Runner)生成目录(可自动生成) │ ├── mocks/ # 存放CMock生成的Mock文件(后续使用) │ └── CMakeLists.txt # 或 Makefile,用于构建测试程序 └── CMakeLists.txt # 主项目构建文件关键点解析:
- 隔离测试代码:将
test/目录与产品src/目录完全分开,保证发布产品时不会包含任何测试代码。 - 集中管理Unity:把Unity框架文件放在
test/unity/下,方便管理和版本升级。 - 测试文件命名:我习惯用
test_前缀加上被测试模块的名字来命名测试文件,例如test_crc.c对应测试crc.c。一目了然。 - 构建系统:在PC上运行测试,意味着你需要一个PC端的构建系统。CMake是跨平台的好选择,简单的项目用Makefile也行。这个构建系统只编译你的被测代码、测试代码和Unity框架,并链接成一个可在PC上运行的可执行文件。
3.2 理解测试固件(Test Fixture)与测试运行器(Test Runner)
这是Unity工作流程中的两个核心概念。
测试固件(Test Fixture): 这可不是硬件里的那个“固件”。在这里,它指的是一组相关的测试用例的集合。通常,一个测试文件(如test_crc.c)就是一个测试固件。这个文件里包含了两类东西:
setUp()和tearDown()函数(可选)。setUp在每个测试用例运行前被调用,用于初始化测试环境(如初始化结构体、分配内存)。tearDown在每个测试用例运行后被调用,用于清理资源(如释放内存、复位状态)。如果你的测试用例彼此独立,不需要特殊的准备和清理,可以不实现它们。- 多个测试用例函数。这些函数名必须以
test_或spec_开头,这是Unity识别它们的约定。
测试运行器(Test Runner): 这是一个main函数所在的文件,它的职责是:
- 调用
UNITY_BEGIN()开始测试。 - 依次调用所有你想运行的测试固件中的测试用例函数。
- 调用
UNITY_END()结束测试,并输出总结报告。
手动编写Test Runner很繁琐,尤其是当测试用例很多时。幸运的是,Unity社区提供了Ruby脚本generate_test_runner.rb,可以自动扫描你的测试文件,生成对应的Test Runner源文件。这是非常关键的一个自动化步骤。
3.3 丰富的断言宏:你验证逻辑的武器库
断言是测试的灵魂。Unity的断言宏定义在unity.h中,数量众多,但掌握几个最常用的就能覆盖80%的场景。关键在于根据数据类型选择正确的断言宏,否则可能得到错误的结果或编译警告。
常用断言宏分类:
| 宏名称 | 用途 | 示例 | 注意事项 |
|---|---|---|---|
| 基本判断 | TEST_ASSERT(condition) | 判断条件为真 | TEST_ASSERT(ptr != NULL) |
| 相等判断 | TEST_ASSERT_EQUAL(expected, actual) | 判断两个基本类型相等 | TEST_ASSERT_EQUAL(5, result) |
TEST_ASSERT_EQUAL_INT(expected, actual) | 判断整型相等 | TEST_ASSERT_EQUAL_INT(100, calculate()) | |
TEST_ASSERT_EQUAL_HEX(expected, actual) | 以十六进制格式判断整型相等 | TEST_ASSERT_EQUAL_HEX(0xAA, reg_value) | |
TEST_ASSERT_EQUAL_STRING(expected, actual) | 判断字符串内容相等 | TEST_ASSERT_EQUAL_STRING("OK", status) | |
TEST_ASSERT_EQUAL_MEMORY(expected, actual, len) | 比较指定长度内存是否一致 | TEST_ASSERT_EQUAL_MEMORY(ref_data, buffer, 256) | |
| 浮点数判断 | TEST_ASSERT_FLOAT_WITHIN(delta, expected, actual) | 判断浮点数在误差范围内相等 | TEST_ASSERT_FLOAT_WITHIN(0.001, 3.14159, pi) |
TEST_ASSERT_EQUAL_FLOAT(expected, actual) | 内部也是用_WITHIN,使用默认精度UNITY_FLOAT_PRECISION | ||
| 大小判断 | TEST_ASSERT_GREATER_THAN(threshold, actual) | 判断实际值大于阈值 | TEST_ASSERT_GREATER_THAN(0, sensor_reading) |
TEST_ASSERT_LESS_THAN(threshold, actual) | 判断实际值小于阈值 | TEST_ASSERT_LESS_THAN(100, temperature) | |
| 真假判断 | TEST_ASSERT_TRUE(condition) | 判断条件为真 | TEST_ASSERT_TRUE(is_valid(input)) |
TEST_ASSERT_FALSE(condition) | 判断条件为假 | TEST_ASSERT_FALSE(error_flag) |
重要经验:对于浮点数比较,务必使用
TEST_ASSERT_FLOAT_WITHIN并指定一个可接受的误差范围(如0.00001)。这是新手最容易踩的坑之一,因为浮点数在计算机中的表示和运算存在精度损失,直接比较“相等”几乎总是失败。
4. 实操过程:从零搭建第一个测试
让我们用一个具体的例子来走通整个流程。假设我们有一个非常简单的项目,里面有一个计算校验和的源文件checksum.c。
4.1 准备被测代码与Unity框架
首先,创建项目结构。我们有一个简单的checksum.c:
// src/checksum.c #include “checksum.h” uint8_t calculate_checksum(const uint8_t *data, uint16_t length) { if (data == NULL || length == 0) { return 0; } uint32_t sum = 0; for (uint16_t i = 0; i < length; i++) { sum += data[i]; } return (uint8_t)(sum & 0xFF); // 返回低8位作为校验和 }对应的头文件checksum.h:
// src/checksum.h #ifndef CHECKSUM_H #define CHECKSUM_H #include <stdint.h> uint8_t calculate_checksum(const uint8_t *data, uint16_t length); #endif接下来,从Unity的GitHub仓库(https://github.com/ThrowTheSwitch/Unity)下载或克隆其源码。我们只需要src/目录下的unity.c、unity.h和unity_internals.h这三个文件。将它们拷贝到我们项目的test/unity/目录下。
4.2 编写第一个测试用例
在test/src/目录下创建测试文件test_checksum.c:
// test/src/test_checksum.c #include “unity.h” // 必须包含Unity头文件 #include “checksum.h” // 包含被测模块的头文件 // 可选:如果所有测试用例都需要一些初始化/清理,可以定义setUp和tearDown void setUp(void) { // 可以在这里初始化一些全局变量或状态 // 本例中不需要,所以函数体为空 } void tearDown(void) { // 清理setUp中分配的资源 // 本例中不需要 } // 测试用例1:正常数据计算 void test_calculate_checksum_normal(void) { uint8_t data[] = {0x01, 0x02, 0x03, 0x04}; uint8_t result = calculate_checksum(data, 4); // 1+2+3+4 = 10 (0x0A) TEST_ASSERT_EQUAL_HEX(0x0A, result); } // 测试用例2:空指针输入 void test_calculate_checksum_null_pointer(void) { uint8_t result = calculate_checksum(NULL, 5); // 根据我们的设计,输入空指针应返回0 TEST_ASSERT_EQUAL(0, result); } // 测试用例3:长度为零 void test_calculate_checksum_zero_length(void) { uint8_t data[] = {0xFF}; uint8_t result = calculate_checksum(data, 0); // 长度为零也应返回0 TEST_ASSERT_EQUAL(0, result); } // 测试用例4:数据溢出(求和超过255) void test_calculate_checksum_overflow(void) { uint8_t data[] = {0xFF, 0x01}; // 255 + 1 = 256, 低8位为0 uint8_t result = calculate_checksum(data, 2); TEST_ASSERT_EQUAL(0, result); }代码解读:
- 每个测试用例都是一个返回
void且无参数的函数,名称以test_开头。 setUp/tearDown在本例中为空,但保留它们是一个好习惯,未来扩展测试时会用到。- 我们设计了四个测试用例,分别覆盖了正常功能、边界条件(空指针、零长度)和特殊情况(溢出)。这就是单元测试的核心:用各种输入去“冲击”你的函数,验证其行为。
- 断言宏选择了
TEST_ASSERT_EQUAL_HEX和TEST_ASSERT_EQUAL,因为结果是整数,用_HEX可以方便地查看十六进制结果。
4.3 生成并理解测试运行器(Test Runner)
手动写一个main函数来调用所有这些test_xxx函数很麻烦。我们使用Unity自带的Ruby脚本来自动生成。
- 确保系统有Ruby环境(Windows可安装RubyInstaller,Mac/Linux通常自带)。
- 将Unity源码中的
auto/目录(包含generate_test_runner.rb脚本)也拷贝到test/目录下,或者记住脚本路径。 - 打开终端,进入项目
test/目录,执行命令:
这会在当前目录生成一个ruby auto/generate_test_runner.rb src/test_checksum.ctest_checksum_runner.c文件。
让我们看一下生成的关键部分:
// test_checksum_runner.c (部分) #include “unity.h” #include “test_checksum.c” // 注意:这里直接包含了测试文件! extern void setUp(void); extern void tearDown(void); extern void test_calculate_checksum_normal(void); extern void test_calculate_checksum_null_pointer(void); extern void test_calculate_checksum_zero_length(void); extern void test_calculate_checksum_overflow(void); int main(void) { UNITY_BEGIN(); // 测试开始 RUN_TEST(test_calculate_checksum_normal, 1); // 第二个参数是行号,用于定位 RUN_TEST(test_calculate_checksum_null_pointer, 8); RUN_TEST(test_calculate_checksum_zero_length, 15); RUN_TEST(test_calculate_checksum_overflow, 22); return UNITY_END(); // 测试结束并返回结果 }生成逻辑解析:
- 脚本会解析
test_checksum.c,找出所有以test_或spec_开头的函数声明。 - 生成一个
main函数,其中按顺序调用RUN_TEST来执行每个测试用例。 RUN_TEST的第二个参数是该测试用例在源文件中的起始行号,当测试失败时,Unity会用这个行号来报告是哪个测试出错了,非常贴心。- 注意:运行器直接
#include “test_checksum.c”,这是一种常见的做法,避免了单独编译测试文件再链接的麻烦。这意味着你的测试文件(test_checksum.c)不能包含main函数。
4.4 编写构建脚本并运行测试
现在我们需要一个CMakeLists.txt来告诉编译器如何构建我们的测试程序。
# test/CMakeLists.txt cmake_minimum_required(VERSION 3.10) project(EmbeddedUnitTest LANGUAGES C) # 设置包含路径 include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/unity ${CMAKE_CURRENT_SOURCE_DIR}/../src # 指向产品源码目录 ) # 添加Unity框架源文件 add_library(unity STATIC unity/unity.c) # 添加被测源码 add_library(checksum_src STATIC ../src/checksum.c) # 创建测试可执行文件 add_executable(test_checksum src/test_checksum.c runner/test_checksum_runner.c # 假设生成的runner放在runner/目录 ) # 链接Unity库和被测代码库 target_link_libraries(test_checksum unity checksum_src)然后,在test/目录下执行经典的CMake构建命令:
mkdir build && cd build cmake .. make编译成功后,运行生成的可执行文件./test_checksum。
4.5 解读测试输出
运行测试程序,你会在终端看到类似这样的输出:
test_checksum.c:10:test_calculate_checksum_normal:PASS test_checksum.c:17:test_calculate_checksum_null_pointer:PASS test_checksum.c:24:test_calculate_checksum_zero_length:PASS test_checksum.c:31:test_calculate_checksum_overflow:PASS ---------------------- 4 Tests 0 Failures 0 Ignored OK输出解读:
- 每一行对应一个测试用例的执行结果,格式为:
文件名:行号:测试函数名:结果。 PASS表示通过。- 最后是总结:共4个测试,0个失败,0个被忽略,总体
OK。
如果某个测试失败了,比如我们故意把第一个测试的期望值改成0x0B,输出会变成:
test_checksum.c:10:test_calculate_checksum_normal:FAIL: Expected 0x0B Was 0x0A ... ---------------------- 4 Tests 1 Failures 0 Ignored FAIL错误信息非常清晰:在test_checksum.c的第10行,test_calculate_checksum_normal测试失败,期望值是0x0B,但实际得到的是0x0A。这能让你立刻定位到问题所在。
5. 常见问题与排查技巧实录
在实际使用Unity的过程中,你肯定会遇到一些坑。下面是我总结的一些常见问题及其解决方法。
5.1 编译与链接问题
问题1:编译时提示找不到UNITY_BEGIN,RUN_TEST等符号。
- 原因:没有正确链接
unity.c。确保你的构建系统(CMake/Makefile)将unity.c编译并链接进了最终的可执行文件。 - 检查:在
test_runner.c生成的main函数里,是否包含了#include “unity.h”?你的构建脚本是否将unity.c加入到了源文件列表?
问题2:链接时出现重复的main函数定义。
- 原因:你的测试文件(
test_xxx.c)或者被测源码中,意外地包含了一个main函数。记住,整个测试程序只能有一个main函数,就是由Test Runner生成的那个。 - 解决:检查你的
.c文件,确保只有test_xxx_runner.c(或你手动编写的统一运行器)里有main函数。
问题3:测试代码需要调用标准库函数(如malloc,printf),但在交叉编译或裸机环境下没有。
- 原因:Unity内部某些功能(如
UNITY_PRINT_EOL)可能默认依赖标准库。在嵌入式环境中,这些可能不可用。 - 解决:通过定义宏来提供自定义实现。最常见的是重定向输出:
同样,如果需要// 在你的测试运行器或某个公共头文件中,在include unity.h之前定义 #ifdef TARGET_EMBEDDED #define UNITY_OUTPUT_CHAR(c) my_putchar(c) // 你的串口发送函数 #define UNITY_OUTPUT_FLUSH() my_uart_flush() #define UNITY_PRINT_EOL() do { UNITY_OUTPUT_CHAR(‘\r’); UNITY_OUTPUT_CHAR(‘\n’); } while (0) #endif #include “unity.h”malloc/free,你可以通过UNITY_MALLOC和UNITY_FREE宏来定义自己的内存管理函数。
5.2 测试执行与断言问题
问题4:浮点数测试总是失败。
- 原因:这是最经典的新手坑。使用了
TEST_ASSERT_EQUAL或TEST_ASSERT_EQUAL_FLOAT(未自定义精度)来比较浮点数。 - 解决:永远使用
TEST_ASSERT_FLOAT_WITHIN(delta, expected, actual)。根据你的精度要求选择合适的delta(例如0.00001f)。float expected = 3.14159f; float actual = calculate_pi(); TEST_ASSERT_FLOAT_WITHIN(0.0001f, expected, actual); // 允许万分之一的误差
问题5:测试数组或结构体时,如何方便地比较?
- 场景:一个函数返回了一个结构体或一个数组,你想验证其内容。
- 解决:使用
TEST_ASSERT_EQUAL_MEMORY。你需要一个已知正确的“预期”数据块。typedef struct { int id; float value; } SensorData; SensorData expected = {.id = 1, .value = 3.14f}; SensorData actual = read_sensor(); TEST_ASSERT_EQUAL_MEMORY(&expected, &actual, sizeof(SensorData));注意:
TEST_ASSERT_EQUAL_MEMORY是逐字节比较,要求内存布局完全一致。如果结构体包含指针(指向动态内存),比较的是指针地址本身,而不是指针指向的内容。
问题6:有些测试用例想暂时跳过不执行怎么办?
- 解决:Unity提供了
TEST_IGNORE()宏。你可以把它放在测试用例函数体的开头。
在测试报告中,这个测试会被标记为void test_some_experimental_feature(void) { TEST_IGNORE(); // 这个测试不会被执行 TEST_ASSERT_EQUAL(42, experimental_func()); }IGNORE,不计入失败。
5.3 测试设计与组织问题
问题7:测试用例之间相互干扰(没有完全独立)。
- 现象:单独运行测试A能过,但连续运行A和B,B就失败。或者顺序调换,结果又不同。
- 原因:测试用例依赖了全局变量或外部状态,且上一个测试修改了该状态,没有清理。
- 解决:
- 使用
setUp和tearDown:将每个测试用例所需的初始化和清理工作放在这两个函数里。确保每个测试开始前环境都是一致的。 - 避免使用全局变量:如果被测函数必须依赖某个全局状态,考虑将其作为参数传入,这样在测试中更容易控制。
- 遵循“单元”测试原则:一个测试只测一个函数或一个很小的功能点,目标明确,不依赖其他未测试的部分。
- 使用
问题8:如何测试静态函数(static function)?
- 挑战:C语言的
static函数只在当前文件内可见,测试代码无法直接调用。 - 常见方案:
- 条件编译:在源文件中,通过宏在测试时改变函数的链接属性。(不推荐,污染产品代码)
然后在测试构建时定义// checksum.c #ifdef UNIT_TEST #define STATIC_TESTABLE #else #define STATIC_TESTABLE static #endif STATIC_TESTABLE uint8_t helper_function(void) { ... }UNIT_TEST宏。 - 将测试代码放在同一个文件:将测试代码直接写在源文件里,用
#ifdef UNIT_TEST包裹。(更不推荐,混合严重) - 最佳实践:不直接测试静态函数。静态函数通常是公有函数的内部辅助函数。你应该通过测试公有函数来间接覆盖静态函数的行为。如果静态函数复杂到需要独立测试,那它可能应该被提取出来成为一个独立的、非静态的函数。这促使你思考更好的模块划分。
- 条件编译:在源文件中,通过宏在测试时改变函数的链接属性。(不推荐,污染产品代码)
问题9:测试输出太多,想只看到失败信息。
- 解决:Unity默认输出所有测试结果。你可以通过定义
UNITY_OUTPUT_COLOR宏(如果终端支持)来获得彩色输出,但无法直接关闭成功信息。一个变通方法是,在验证通过后,将测试结果重定向到一个文件,然后用脚本分析。或者,你可以修改Unity的源码(unity.c中的UnityPrint相关函数),但一般不推荐。
5.4 进阶技巧:测试驱动开发(TDD)与持续集成
当你熟悉了基础测试后,可以尝试更高效的开发模式:
- 测试驱动开发:在写产品代码之前,先写测试用例。比如,你要实现一个
filter_data函数,先想好它的接口和预期行为,然后在test_filter.c里写下test_filter_should_remove_negative_values等测试用例。此时运行测试,肯定是失败的(红)。然后你再开始写filter_data的实现,直到所有测试通过(绿)。这个过程能帮你厘清需求,设计出更合理的接口。 - 与CI/CD集成:将你的测试套件集成到像Jenkins、GitLab CI这样的持续集成工具中。每次代码提交后,自动在服务器上拉取代码、编译、运行所有单元测试。任何导致测试失败的提交都会被立即发现,保证主分支代码的稳定性。对于嵌入式项目,这通常意味着CI服务器上需要安装对应的交叉编译工具链。
Unity的初体验之旅到这里就差不多了。它就像一把钥匙,打开了一扇通往更稳健、更可维护的嵌入式软件开发的大门。一开始可能会觉得写测试代码有点繁琐,但当你第一次因为它而避免了一个深夜的调试,或者自信地重构了一段复杂代码而所有测试依然绿灯时,你就会觉得这一切都是值得的。
