Unity 断言参考:C 单元测试框架 Unity 的断言体系完全指南与实现原理解析
Unity 断言参考:C 单元测试框架 Unity 的断言体系完全指南与实现原理解析
Unity 是一个以"断言(Assertion)"为核心的轻量级 C 单元测试框架,docs/UnityAssertionsReference.md 是官方对这套断言体系的权威参考文档。本文以该文档为骨架,完整收录 Unity 全部断言宏的语义、命名约定与参数约定,并结合仓库中的 src/unity.h、src/unity_internals.h、src/unity.c 源码及 test/tests/ 下的测试用例,深入讲解浮点相等断言的工作原理、非标准整数宽度的适配方案等进阶主题。读完本文,你将能够熟练为任意 C 类型选择恰当的断言宏,读懂并定制断言失败信息,以及在嵌入式等特殊目标平台上正确配置 Unity。
背景与概述
极简版(Super Condensed Version)
- 断言建立事实:一个断言用于确立某个单一条件的真假(布尔真值)。当条件为假时,断言会停止执行并报告失败。
- Unity 的本质:Unity 主要就是一个丰富的断言集合,外加把这些断言收集起来并轻松执行的配套框架。
- 测试与源码分离:Unity 的结构使得你可以很自然地把测试断言与源码分开,即"测试代码"与"产品代码"各归其位。
- Unity 断言的特点:
- 种类繁多,覆盖各种 C 类型与断言场景;
- 借助上下文(context)提供详尽而有帮助的失败消息;
- 断言本身就是一种"免费"的源码文档——它们记录了类型、期望值以及基本行为。
Unity 是什么:主要就是断言
一种理解 Unity 的方式是:它就是一个丰富的断言集合,用于确认你的源码是否真的按照你设想的方式工作。同时 Unity 提供了框架,让你可以方便地把这些断言组织进独立于源码之外的测试代码中,并逐一执行它们。
什么是断言?
断言的核心是确立"真"——布尔意义上的真。这个东西是否等于那个东西?那段代码是否具有某某属性?断言是可执行代码。静态分析是提升代码质量的有效手段,但它无法像断言那样真正"执行"你的代码。一个失败的断言会中止执行,并通过某种适当的 I/O 通道(stdout、GUI、输出文件,甚至一个闪烁的指示灯)报告错误。
从本质上讲,做动态验证,你其实只需要一个最基础的断言机制就够了——C 标准库中的 assert() 宏就是干这个的。那为什么不用它?因为报告能力可以做得远比它好。C 的 assert() 本身相当"笨",尤其不擅长处理数组、结构体这类常见数据类型;而且如果没有额外约束,很容易让人忍不住往源码里撒满 assert()。像 Unity 这样把测试代码与源码分离开来,通常更干净、更可控、也更有用。
Unity 断言:帮助性消息与"免费的源码文档"
仅仅断言一个简单条件是有价值的,但利用断言的上下文更有价值。例如,当你知道自己比较的是位标志(bit flags)而不是普通整数时,为什么不借助这个上下文,在断言失败时给出明确的、可读的位级反馈?
这正是 Unity 断言集合做的事情——捕获上下文,给出有帮助、有意义的失败消息。事实上,断言本身也充当了源码中关于类型与值的可执行文档:只要你的测试与源码保持同步并且全部通过,你就拥有了一份关于源码意图与机制的、随时更新的详细视图。而且,有一个奇妙的规律:测试良好的代码往往也就是设计良好的代码。
断言约定与配置
命名与参数约定
断言的参数顺序遵循如下通用形式:
TEST_ASSERT_X( {modifiers}, {expected}, actual, {size/count} )
actual:被测试的值。它与其他参数不同——所有断言变体都包含actual,它是在所有断言构造中唯一必然存在的参数。modifiers:掩码(mask)、范围(range)、位标志说明、浮点 delta 等修饰参数。expected:你期望的值,用于与actual比较;它被标记为可选参数,因为有些断言只需要一个actual参数(例如空指针检查)。size/count:字符串长度、数组元素个数等。
Unity 的很多断言在数据类型的处理上是"明显重复"的——同一个数据类型往往由多个断言处理,区别在于失败消息的呈现方式。例如,_HEX 变体的断言会把期望值与实际值以十六进制格式打印出来。
TEST_ASSERT_X_MESSAGE 变体
所有断言都配有带字符串消息的变体,消息作为最后一个参数。你指定的字符串会被追加到 Unity 输出中的断言失败消息之后。
为简洁起见,本文参考列表中没有逐一列出带消息参数的变体。你只需要:在下方任意断言名后面加上 _MESSAGE,并把一个字符串作为最后一个参数即可。
示例:
TEST_ASSERT_X( {modifiers}, {expected}, actual, {size/count} )
变成带消息的版本:
TEST_ASSERT_X_MESSAGE( {modifiers}, {expected}, actual, {size/count}, message )
注意事项:
_MESSAGE变体刻意不支持printf风格的格式化,因为很多嵌入式项目出于各种原因不支持或避免使用printf。必要时,可以在断言之前用sprintf拼装一条复杂的失败消息。- 如果你需要在断言失败消息里输出计数器之类的值(比如循环中的索引),与其用
sprintf,不如把结果先收集进数组,然后用某个_ARRAY断言(见下文)来输出,这可能更方便。
TEST_ASSERT_X_ARRAY 变体
Unity 为包含各种类型的数组提供了一套断言(详见下文"数组"一节)。它与 _MESSAGE 变体地位几乎相当:几乎任何 Unity 类型断言都可以缀上 _ARRAY,对一整块内存执行断言。
TEST_ASSERT_EQUAL_TYPEX_ARRAY( expected, actual, {size/count} )
expected本身是一个数组。size/count是一个或两个参数,用于确定数组元素个数,以及(可能需要的)数组内元素的长度。
注意:
_MESSAGE变体约定在这里依然适用:_ARRAY断言的_MESSAGE变体以_ARRAY_MESSAGE结尾。- 处理浮点数组的断言被归并到 float 与 double 断言一节(见下文)。
TEST_ASSERT_EACH_EQUAL_X 变体
Unity 还提供了一套断言,用于把各种类型的数组与单个值逐一比较(详见下文"Each Equal"一节)。它与 _MESSAGE 变体地位几乎相当:几乎任何 Unity 类型断言都可以注入 _EACH_EQUAL,对一整块内存执行断言。
TEST_ASSERT_EACH_EQUAL_TYPEX( expected, actual, {size/count} )
expected是一个要与数组元素比较的单个值。actual是数组,其中每个元素都会被拿来与期望值比较。size/count是用于确定数组元素个数的一个或两个参数。
注意:
_MESSAGE变体约定在这里依然适用于 Each Equal 断言。- 浮点值的 Each Equal 断言归并到 float 与 double 断言一节。
配置
浮点支持是可选的
对浮点类型的支持是可配置的:通过定义合适的预处理器符号,float 和 double 可以在 Unity 代码中分别启用或禁用。这对于没有浮点数学支持的嵌入式目标很有用(即 Unity 可以在仅定点运算的平台上无错误地编译)。具体配置项(UNITY_EXCLUDE_FLOAT、UNITY_INCLUDE_DOUBLE、UNITY_EXCLUDE_DOUBLE 等)参见仓库内的 Unity 配置指南。
最大数据类型宽度是可配置的
并非所有目标都支持 64 位宽的类型,甚至不是都支持 32 位宽类型。定义合适的预处理器符号后,Unity 会从编译中省略所有超出目标最大宽度的操作。相关配置项(UNITY_INT_WIDTH、UNITY_LONG_WIDTH、UNITY_POINTER_WIDTH、UNITY_SUPPORT_64)同样参见 Unity 配置指南。
断言大全(The Assertions in All Their Blessed Glory)
基础:失败、通过与忽略
TEST_FAIL()
TEST_FAIL_MESSAGE("message")
这个宏最常用于测试代码中需要执行超出简单断言逻辑的特殊场景。实践中,TEST_FAIL() 几乎总是出现在某个条件代码块内部。
典型示例:
- 多次执行某个状态机,由测试代码递增计数器并在最后一步验证;
- 触发异常并验证它(例如 Try / Catch / Throw 模式,参见 ThrowTheSwitch 的 CException 项目)。
TEST_PASS()
TEST_PASS_MESSAGE("message")
这将中止测试的剩余部分,但把该测试计为通过。正常情况下,你的测试中没必要包含这个宏——没有失败就会自动被计为 PASS。不过它对带有 #ifdef 之类结构的测试偶尔会有用。在 src/unity.h 中可以看到其实现:
#define TEST_PASS() TEST_ABORT()
#define TEST_PASS_MESSAGE(message) do { UnityMessage((message), __LINE__); TEST_ABORT(); } while (0)
TEST_IGNORE()
TEST_IGNORE_MESSAGE("message")
把某个测试用例(即用于承载测试断言的函数)标记为忽略。通常用作"回头再实现这个测试用例"的路标。忽略一个测试用例对同一测试用例内其他断言会产生影响(详见 Unity 文档)。
TEST_MESSAGE(message)
用于向 Unity 输出流输出 INFO 消息,而不会结束测试。与 pass/fail 消息一样,它会连同文件名和行号一起输出。其底层实现为 src/unity.h 中的 UnityMessage((message), __LINE__)。
布尔(Boolean)
TEST_ASSERT (condition)
TEST_ASSERT_TRUE (condition)
TEST_ASSERT_FALSE (condition)
TEST_ASSERT_UNLESS (condition)
TEST_ASSERT_UNLESS 只是 TEST_ASSERT_FALSE 的一个措辞变体,其语义有助于提升某些测试构造或条件语句的可读性。
TEST_ASSERT_NULL (pointer)
TEST_ASSERT_NOT_NULL (pointer)
验证指针是否为 NULL 或非 NULL。
TEST_ASSERT_EMPTY (pointer)
TEST_ASSERT_NOT_EMPTY (pointer)
验证从指针解引用的第一个元素是否为零(或非零)。这对于检查空(或非空)的以空字符结尾的 C 字符串特别有用,同样也适用于其他以 NULL 结尾的数组。
在 src/unity.h 中,布尔断言的失败消息文本可以直接看到,例如 " Expression Evaluated To FALSE"、" Expected TRUE Was FALSE"、" Expected NULL"、" Expected Empty" 等。
有符号与无符号整数(各种尺寸)
大尺寸整数类型可以被禁用,以适配不支持的构建目标。例如,如果你的目标只支持 16 位类型,通过定义合适的符号,Unity 可以配置为省略会破坏编译的 32 位和 64 位操作(详见 Unity 文档)。关于其他字长的处理建议,参见本文后面的"高级断言"一节。
TEST_ASSERT_EQUAL_INT (expected, actual)
TEST_ASSERT_EQUAL_INT8 (expected, actual)
TEST_ASSERT_EQUAL_INT16 (expected, actual)
TEST_ASSERT_EQUAL_INT32 (expected, actual)
TEST_ASSERT_EQUAL_INT64 (expected, actual)
TEST_ASSERT_EQUAL_UINT (expected, actual)
TEST_ASSERT_EQUAL_UINT8 (expected, actual)
TEST_ASSERT_EQUAL_UINT16 (expected, actual)
TEST_ASSERT_EQUAL_UINT32 (expected, actual)
TEST_ASSERT_EQUAL_UINT64 (expected, actual)
从 src/unity.h 的宏定义可以看出,这些宏全部映射到 unity_internals.h 中的 UNITY_TEST_ASSERT_EQUAL_* 实现,例如:
#define TEST_ASSERT_EQUAL_INT(expected, actual) UNITY_TEST_ASSERT_EQUAL_INT((expected), (actual), __LINE__, NULL)
#define TEST_ASSERT_EQUAL_UINT64(expected, actual) UNITY_TEST_ASSERT_EQUAL_UINT64((expected), (actual), __LINE__, NULL)
仓库还额外提供了 TEST_ASSERT_EQUAL_size_t 系列(映射到 UINT 实现)、TEST_ASSERT_NOT_EQUAL_* 系列(不等断言)以及 TEST_ASSERT_EQUAL_HEX8/16/32/64 等变体。整数的相等/不等断言均有对应的测试用例覆盖,见 test/tests/test_unity_integers.c 与 test/tests/test_unity_integers_64.c。
十六进制无符号整数(各种尺寸)
所有 _HEX 断言与无符号整数断言在功能上完全相同,但失败消息中的 expected 和 actual 值以十六进制格式输出。Unity 输出为大端(big endian)格式。
TEST_ASSERT_EQUAL_HEX (expected, actual)
TEST_ASSERT_EQUAL_HEX8 (expected, actual)
TEST_ASSERT_EQUAL_HEX16 (expected, actual)
TEST_ASSERT_EQUAL_HEX32 (expected, actual)
TEST_ASSERT_EQUAL_HEX64 (expected, actual)
注意,不带后缀的 TEST_ASSERT_EQUAL_HEX 实际映射到 UNITY_TEST_ASSERT_EQUAL_HEX32(见 src/unity.h),即以 32 位宽度比较。
字符(Characters)
虽然你可以用 8 位整数断言来比较 char,但另一种选择是使用这个专门的断言:它会将可打印字符显示为可打印字符,否则显示字符的 HEX 转义码。
TEST_ASSERT_EQUAL_CHAR (expected, actual)
掩码与位级断言(Masked and Bit-level Assertions)
掩码与位级断言以十六进制格式输出结果。Unity 输出为大端格式。
TEST_ASSERT_BITS (mask, expected, actual)
只比较 expected 与 actual 参数中被掩码覆盖(即高位)的位。
TEST_ASSERT_BITS_HIGH (mask, actual)
断言 actual 参数中被掩码覆盖的位为高。
TEST_ASSERT_BITS_LOW (mask, actual)
断言 actual 参数中被掩码覆盖的位为低。
TEST_ASSERT_BIT_HIGH (bit, actual)
断言 actual 参数中指定的一位为高。
TEST_ASSERT_BIT_LOW (bit, actual)
断言 actual 参数中指定的一位为低。
从源码看,位断言其实是 UNITY_TEST_ASSERT_BITS 的封装:TEST_ASSERT_BITS_HIGH(mask, actual) 等价于用 (UNITY_UINT)(-1) 作为期望值、TEST_ASSERT_BITS_LOW(mask, actual) 等价于用 (UNITY_UINT)(0) 作为期望值,而 TEST_ASSERT_BIT_HIGH/BIT_LOW 则把 (UNITY_UINT)1 << (bit) 作为掩码(见 src/unity.h)。
整数小于 / 大于(Integer Less Than / Greater Than)
这些断言验证 actual 参数严格小于或严格大于 threshold(不包含边界)。例如,若 greater-than 断言的阈值为 0,则实际值等于 0 或小于 0 都会失败。与相等断言一样,这些断言覆盖所有尺寸的整数。部分示例:
TEST_ASSERT_GREATER_THAN_INT8 (threshold, actual)
TEST_ASSERT_GREATER_OR_EQUAL_INT16 (threshold, actual)
TEST_ASSERT_LESS_THAN_INT32 (threshold, actual)
TEST_ASSERT_LESS_OR_EQUAL_UINT (threshold, actual)
TEST_ASSERT_NOT_EQUAL_UINT8 (threshold, actual)
从 src/unity.h 可以看到,还有 TEST_ASSERT_GREATER_THAN、TEST_ASSERT_LESS_THAN、TEST_ASSERT_GREATER_OR_EQUAL、TEST_ASSERT_LESS_OR_EQUAL 等不带尺寸后缀的通用形式(分别映射到 INT/UINT 实现),以及覆盖 size_t、HEX、CHAR 的完整变体。
整数范围(各种尺寸)
这些断言验证 expected 参数是否在 actual 参数的 ±delta(含边界)范围内。例如,期望值为 10、delta 为 3 时,任何超出 7~13 范围的值都会使断言失败。
TEST_ASSERT_INT_WITHIN (delta, expected, actual)
TEST_ASSERT_INT8_WITHIN (delta, expected, actual)
TEST_ASSERT_INT16_WITHIN (delta, expected, actual)
TEST_ASSERT_INT32_WITHIN (delta, expected, actual)
TEST_ASSERT_INT64_WITHIN (delta, expected, actual)
TEST_ASSERT_UINT_WITHIN (delta, expected, actual)
TEST_ASSERT_UINT8_WITHIN (delta, expected, actual)
TEST_ASSERT_UINT16_WITHIN (delta, expected, actual)
TEST_ASSERT_UINT32_WITHIN (delta, expected, actual)
TEST_ASSERT_UINT64_WITHIN (delta, expected, actual)
TEST_ASSERT_HEX_WITHIN (delta, expected, actual)
TEST_ASSERT_HEX8_WITHIN (delta, expected, actual)
TEST_ASSERT_HEX16_WITHIN (delta, expected, actual)
TEST_ASSERT_HEX32_WITHIN (delta, expected, actual)
TEST_ASSERT_HEX64_WITHIN (delta, expected, actual)
TEST_ASSERT_CHAR_WITHIN (delta, expected, actual)
同样地,不带后缀的 TEST_ASSERT_HEX_WITHIN 映射到 UNITY_TEST_ASSERT_HEX32_WITHIN,而 TEST_ASSERT_size_t_WITHIN 映射到 UINT 实现(见 src/unity.h)。
结构体与字符串(Structs and Strings)
TEST_ASSERT_EQUAL_PTR (expected, actual)
断言两个指针指向同一个内存位置。
TEST_ASSERT_NOT_EQUAL_PTR (expected, actual)
断言两个指针不指向同一个内存位置。两个 NULL 指针被视为相等,会使此断言失败;NULL 指针与非 NULL 指针被视为不相等,会通过。
TEST_ASSERT_EQUAL_STRING (expected, actual)
断言两个以空字符('\0')结尾的字符串完全相同。如果字符串长度不同,或终止符之前的任何部分不同,断言失败。两个 NULL 字符串(即零长度)被视为等价。
TEST_ASSERT_NOT_EQUAL_STRING (expected, actual)
断言两个以空字符结尾的字符串不相同。只要字符串在空终止符之前的任何位置存在长度或内容差异,断言就通过。NULL 指针与非 NULL 字符串被视为不相等,会通过;两个 NULL 指针被视为相等,会失败。
TEST_ASSERT_EQUAL_STRING_LEN (expected, actual, len)
断言两个以空字符结尾的字符串在前 len 个字符内相同。它只检查前 len 个字符,而不检查整个字符串。
TEST_ASSERT_NOT_EQUAL_STRING_LEN (expected, actual, len)
断言两个以空字符结尾的字符串在前 len 个字符内不相同。只要在指定长度内任意位置存在差异,断言就通过。NULL 指针与非 NULL 字符串被视为不相等,会通过;两个 NULL 指针被视为相等,会失败。指定 len 为 0 被认为毫无意义,会失败。
TEST_ASSERT_EQUAL_MEMORY (expected, actual, len)
断言 expected 与 actual 指针所指向的内存内容完全相同。内存块大小(字节数)由 len 参数指定。
TEST_ASSERT_NOT_EQUAL_MEMORY (expected, actual, len)
断言 expected 与 actual 指针所指向的内存内容在前 len 个字节内不相同。只要该范围内有任意字节不同,断言就通过。NULL 指针与非 NULL 指针被视为不相等,会通过;两个 NULL 指针(或同一个指针)被视为相等,会失败。指定 len 为 0 被认为毫无意义,会失败。
这些宏在 src/unity.h 中均有对应定义,并映射到 UNITY_TEST_ASSERT_EQUAL_* 底层实现。
数组(Arrays)
expected 与 actual 参数都是数组。num_elements 指定要比较的数组元素个数。
_HEX 断言的失败消息会把期望数组与实际数组的内容以十六进制格式输出。
数组的字符串比较行为参见前文 TEST_ASSERT_EQUAL_STRING 的说明。
断言在比较数组中发现第一个不匹配的元素时即失败。失败消息会指出失败比较的数组索引。
TEST_ASSERT_EQUAL_INT_ARRAY (expected, actual, num_elements)
TEST_ASSERT_EQUAL_INT8_ARRAY (expected, actual, num_elements)
TEST_ASSERT_EQUAL_INT16_ARRAY (expected, actual, num_elements)
TEST_ASSERT_EQUAL_INT32_ARRAY (expected, actual, num_elements)
TEST_ASSERT_EQUAL_INT64_ARRAY (expected, actual, num_elements)
TEST_ASSERT_EQUAL_UINT_ARRAY (expected, actual, num_elements)
TEST_ASSERT_EQUAL_UINT8_ARRAY (expected, actual, num_elements)
TEST_ASSERT_EQUAL_UINT16_ARRAY (expected, actual, num_elements)
TEST_ASSERT_EQUAL_UINT32_ARRAY (expected, actual, num_elements)
TEST_ASSERT_EQUAL_UINT64_ARRAY (expected, actual, num_elements)
TEST_ASSERT_EQUAL_HEX_ARRAY (expected, actual, num_elements)
TEST_ASSERT_EQUAL_HEX8_ARRAY (expected, actual, num_elements)
TEST_ASSERT_EQUAL_HEX16_ARRAY (expected, actual, num_elements)
TEST_ASSERT_EQUAL_HEX32_ARRAY (expected, actual, num_elements)
TEST_ASSERT_EQUAL_HEX64_ARRAY (expected, actual, num_elements)
TEST_ASSERT_EQUAL_CHAR_ARRAY (expected, actual, num_elements)
TEST_ASSERT_EQUAL_PTR_ARRAY (expected, actual, num_elements)
TEST_ASSERT_EQUAL_STRING_ARRAY (expected, actual, num_elements)
TEST_ASSERT_EQUAL_MEMORY_ARRAY (expected, actual, len, num_elements)
其中 len 是每个数组元素上要比较的内存字节数。
这些数组断言的测试用例见 test/tests/test_unity_arrays.c。从 src/unity.h 可以看出,TEST_ASSERT_EQUAL_HEX_ARRAY 同样映射到 32 位宽度实现,且存在 TEST_ASSERT_EQUAL_size_t_ARRAY 变体。
整数数组范围(各种尺寸)
这些断言验证 expected 数组参数是否在 actual 数组参数的 ±delta(含边界)范围内。例如,若期望数组为 [10, 12]、delta 为 3,则任何超出 [7-13, 9-15] 范围的值都会使断言失败。
TEST_ASSERT_INT_ARRAY_WITHIN (delta, expected, actual, num_elements)
TEST_ASSERT_INT8_ARRAY_WITHIN (delta, expected, actual, num_elements)
TEST_ASSERT_INT16_ARRAY_WITHIN (delta, expected, actual, num_elements)
TEST_ASSERT_INT32_ARRAY_WITHIN (delta, expected, actual, num_elements)
TEST_ASSERT_INT64_ARRAY_WITHIN (delta, expected, actual, num_elements)
TEST_ASSERT_UINT_ARRAY_WITHIN (delta, expected, actual, num_elements)
TEST_ASSERT_UINT8_ARRAY_WITHIN (delta, expected, actual, num_elements)
TEST_ASSERT_UINT16_ARRAY_WITHIN (delta, expected, actual, num_elements)
TEST_ASSERT_UINT32_ARRAY_WITHIN (delta, expected, actual, num_elements)
TEST_ASSERT_UINT64_ARRAY_WITHIN (delta, expected, actual, num_elements)
TEST_ASSERT_HEX_ARRAY_WITHIN (delta, expected, actual, num_elements)
TEST_ASSERT_HEX8_ARRAY_WITHIN (delta, expected, actual, num_elements)
TEST_ASSERT_HEX16_ARRAY_WITHIN (delta, expected, actual, num_elements)
TEST_ASSERT_HEX32_ARRAY_WITHIN (delta, expected, actual, num_elements)
TEST_ASSERT_HEX64_ARRAY_WITHIN (delta, expected, actual, num_elements)
TEST_ASSERT_CHAR_ARRAY_WITHIN (delta, expected, actual, num_elements)
Each Equal(数组与单值比较)
expected 是单个值,actual 是数组。num_elements 指定要比较的数组元素个数。
_HEX 断言的失败消息会把期望值与实际数组内容以十六进制格式输出。
断言在比较数组中发现第一个不匹配的元素时即失败。失败消息会指出失败比较的数组索引。
TEST_ASSERT_EACH_EQUAL_INT (expected, actual, num_elements)
TEST_ASSERT_EACH_EQUAL_INT8 (expected, actual, num_elements)
TEST_ASSERT_EACH_EQUAL_INT16 (expected, actual, num_elements)
TEST_ASSERT_EACH_EQUAL_INT32 (expected, actual, num_elements)
TEST_ASSERT_EACH_EQUAL_INT64 (expected, actual, num_elements)
TEST_ASSERT_EACH_EQUAL_UINT (expected, actual, num_elements)
TEST_ASSERT_EACH_EQUAL_UINT8 (expected, actual, num_elements)
TEST_ASSERT_EACH_EQUAL_UINT16 (expected, actual, num_elements)
TEST_ASSERT_EACH_EQUAL_UINT32 (expected, actual, num_elements)
TEST_ASSERT_EACH_EQUAL_UINT64 (expected, actual, num_elements)
TEST_ASSERT_EACH_EQUAL_HEX (expected, actual, num_elements)
TEST_ASSERT_EACH_EQUAL_HEX8 (expected, actual, num_elements)
TEST_ASSERT_EACH_EQUAL_HEX16 (expected, actual, num_elements)
TEST_ASSERT_EACH_EQUAL_HEX32 (expected, actual, num_elements)
TEST_ASSERT_EACH_EQUAL_HEX64 (expected, actual, num_elements)
TEST_ASSERT_EACH_EQUAL_CHAR (expected, actual, num_elements)
TEST_ASSERT_EACH_EQUAL_PTR (expected, actual, num_elements)
TEST_ASSERT_EACH_EQUAL_STRING (expected, actual, num_elements)
TEST_ASSERT_EACH_EQUAL_MEMORY (expected, actual, len, num_elements)
其中 len 是每个数组元素上要比较的内存字节数。
浮点(Floating Point,启用后可用)
TEST_ASSERT_FLOAT_WITHIN (delta, expected, actual)
断言 actual 值在 expected 值的 ±delta 范围内。由于浮点表示的本质,精确的相等性评估无法得到保证。
TEST_ASSERT_FLOAT_NOT_WITHIN (delta, expected, actual)
断言 actual 值不在 expected 值的 ±delta 范围内。
TEST_ASSERT_EQUAL_FLOAT (expected, actual)
断言 actual 值"足够接近,可视为等于" expected 值。细节见本文"高级断言"一节。省略用户指定的 delta 既是简洁写法的便利,也是 CMock 代码生成约定的要求。
TEST_ASSERT_NOT_EQUAL_FLOAT (expected, actual)
断言 actual 值"并非足够接近,不能视为等于" expected 值。
TEST_ASSERT_FLOAT_ARRAY_WITHIN (delta, expected, actual, num_elements)
参见数组断言一节。注意:单个数组元素比较时使用用户提供的 delta 加上默认比较 delta 进行检查,其基础是 TEST_ASSERT_FLOAT_WITHIN 比较。
TEST_ASSERT_EQUAL_FLOAT_ARRAY (expected, actual, num_elements)
参见数组断言一节。注意:单个数组元素的浮点比较使用 TEST_ASSERT_EQUAL_FLOAT 执行。也就是说,需要用户指定 delta 的浮点数组比较需要自定义实现的浮点数组断言。
TEST_ASSERT_LESS_THAN_FLOAT (threshold, actual)
断言 actual 参数严格小于 threshold(不包含边界)。例如阈值为 1.0f 时,实际值大于等于 1.0f 都会失败。
TEST_ASSERT_GREATER_THAN_FLOAT (threshold, actual)
断言 actual 参数严格大于 threshold(不包含边界)。例如阈值为 1.0f 时,实际值小于等于 1.0f 都会失败。
TEST_ASSERT_LESS_OR_EQUAL_FLOAT (threshold, actual)
断言 actual 参数小于或等于 threshold。相等性规则与 TEST_ASSERT_EQUAL_FLOAT 相同。
TEST_ASSERT_GREATER_OR_EQUAL_FLOAT (threshold, actual)
断言 actual 参数大于或等于 threshold。相等性规则与 TEST_ASSERT_EQUAL_FLOAT 相同。
TEST_ASSERT_FLOAT_IS_INF (actual)
断言 actual 参数等价于正无穷的浮点表示。
TEST_ASSERT_FLOAT_IS_NEG_INF (actual)
断言 actual 参数等价于负无穷的浮点表示。
TEST_ASSERT_FLOAT_IS_NAN (actual)
断言 actual 参数是 Not A Number(NaN)浮点表示。
TEST_ASSERT_FLOAT_IS_DETERMINATE (actual)
断言 actual 参数是可用于数学运算的浮点表示,即既不是正无穷、负无穷,也不是 NaN。
TEST_ASSERT_FLOAT_IS_NOT_INF (actual)
断言 actual 参数是正无穷浮点表示之外的值。
TEST_ASSERT_FLOAT_IS_NOT_NEG_INF (actual)
断言 actual 参数是负无穷浮点表示之外的值。
TEST_ASSERT_FLOAT_IS_NOT_NAN (actual)
断言 actual 参数是 NaN 浮点表示之外的值。
TEST_ASSERT_FLOAT_IS_NOT_DETERMINATE (actual)
断言 actual 参数不能用于数学运算,即它是正无穷、负无穷或 NaN 浮点表示之一。
这些特殊值断言在 src/unity.c 中由 UnityAssertFloatSpecial 实现(处理 UNITY_FLOAT_IS_INF、UNITY_FLOAT_IS_NEG_INF、UNITY_FLOAT_IS_NAN、UNITY_FLOAT_IS_DETERMINATE 等特例分支),NaN/Inf 的判定则依赖 src/unity_internals.h 中的 UNITY_IS_NAN 与 UNITY_IS_INF 宏。仓库提供的 test/tests/test_unity_floats.c 覆盖了大量边界场景(如 testFloatsNotEqualActualNaN、testFloatsEqualBothInf、testFloatsNotEqualPlusMinusInf 等)。
双精度(Double,启用后可用)
TEST_ASSERT_DOUBLE_WITHIN (delta, expected, actual)
断言 actual 值在 expected 值的 ±delta 范围内。由于浮点表示的本质,精确的相等性评估无法得到保证。
TEST_ASSERT_DOUBLE_NOT_WITHIN (delta, expected, actual)
断言 actual 值不在 expected 值的 ±delta 范围内。
TEST_ASSERT_EQUAL_DOUBLE (expected, actual)
断言 actual 值"足够接近,可视为等于" expected 值。细节见本文"高级断言"一节。省略用户指定的 delta 既是简洁写法的便利,也是 CMock 代码生成约定的要求。
TEST_ASSERT_NOT_EQUAL_DOUBLE (expected, actual)
断言 actual 值"并非足够接近,不能视为等于" expected 值。
TEST_ASSERT_DOUBLE_ARRAY_WITHIN (delta, expected, actual, num_elements)
参见数组断言一节。注意:单个数组元素比较时使用用户提供的 delta 加上默认比较 delta 进行检查,其基础是 TEST_ASSERT_DOUBLE_WITHIN 比较。
TEST_ASSERT_EQUAL_DOUBLE_ARRAY (expected, actual, num_elements)
参见数组断言一节。注意:单个数组元素的 double 比较使用 TEST_ASSERT_EQUAL_DOUBLE 执行。也就是说,需要用户指定 delta 的 double 数组比较需要自定义实现的 double 数组断言。
TEST_ASSERT_LESS_THAN_DOUBLE (threshold, actual)
断言 actual 参数严格小于 threshold(不包含边界)。例如阈值为 1.0 时,实际值大于等于 1.0 都会失败。
TEST_ASSERT_LESS_OR_EQUAL_DOUBLE (threshold, actual)
断言 actual 参数小于或等于 threshold。相等性规则与 TEST_ASSERT_EQUAL_DOUBLE 相同。
TEST_ASSERT_GREATER_THAN_DOUBLE (threshold, actual)
断言 actual 参数严格大于 threshold(不包含边界)。例如阈值为 1.0 时,实际值小于等于 1.0 都会失败。
TEST_ASSERT_GREATER_OR_EQUAL_DOUBLE (threshold, actual)
断言 actual 参数大于或等于 threshold。相等性规则与 TEST_ASSERT_EQUAL_DOUBLE 相同。
TEST_ASSERT_DOUBLE_IS_INF (actual)
断言 actual 参数等价于正无穷的浮点表示。
TEST_ASSERT_DOUBLE_IS_NEG_INF (actual)
断言 actual 参数等价于负无穷的浮点表示。
TEST_ASSERT_DOUBLE_IS_NAN (actual)
断言 actual 参数是 NaN 浮点表示。
TEST_ASSERT_DOUBLE_IS_DETERMINATE (actual)
断言 actual 参数是可用于数学运算的浮点表示,即既不是正无穷、负无穷,也不是 NaN。
TEST_ASSERT_DOUBLE_IS_NOT_INF (actual)
断言 actual 参数是正无穷浮点表示之外的值。
TEST_ASSERT_DOUBLE_IS_NOT_NEG_INF (actual)
断言 actual 参数是负无穷浮点表示之外的值。
TEST_ASSERT_DOUBLE_IS_NOT_NAN (actual)
断言 actual 参数是 NaN 浮点表示之外的值。
TEST_ASSERT_DOUBLE_IS_NOT_DETERMINATE (actual)
断言 actual 参数不能用于数学运算,即它是正无穷、负无穷或 NaN 浮点表示之一。
高级断言:棘手断言的实现细节
本节帮助你理解如何处理一些棘手的断言场景,并一窥 Unity 断言机制幕后的细节。如果你喜欢了解背景原理,请继续读下去;否则可以跳过本节,等需要时再回来查阅。
EQUAL 断言在 FLOAT 和 DOUBLE 上是如何工作的?
你可能知道,直接对一对 float 或一对 double 做相等性检查,往好里说是马虎,往坏里说是大忌。浮点值常常有多种表示方式,尤其是在对一个值做了一系列运算之后。把变量初始化为 2.0,很可能得到一个 2 × 2⁰ 的浮点表示;但一系列数学运算之后,可能会得到一个同样求值为 2 的 8 × 2⁻² 表示。到某个时刻,反复的运算会让相等性检查失败。
所以 Unity 不做浮点值的直接相等比较,而是检查两个浮点值是否"真的非常接近"。如果你让 Unity 保持默认配置,那么"非常接近"意味着"相差一到两个有效位以内"。在底层,TEST_ASSERT_EQUAL_FLOAT 实际上是 TEST_ASSERT_FLOAT_WITHIN,其中 delta 参数是当场计算出来的。对单精度而言,delta 等于期望值乘以 0.00001,在期望值周围产生一个非常小的比例范围。
这一实现可以直接在源码中看到。在 src/unity_internals.h 中:
#define UNITY_FLOAT_PRECISION (0.00001f)
#define UNITY_DOUBLE_PRECISION (1e-12)
以及:
#define UNITY_TEST_ASSERT_EQUAL_FLOAT(expected, actual, line, message) \
UNITY_TEST_ASSERT_FLOAT_WITHIN((UNITY_FLOAT)(expected) * (UNITY_FLOAT)UNITY_FLOAT_PRECISION, \
(UNITY_FLOAT)(expected), (UNITY_FLOAT)(actual), line, message)
也就是说,EQUAL 断言完全是在运行时把 expected * PRECISION 当作 delta,转成 WITHIN 断言执行的。
举例说明:如果你期望的值为 20000.0,那么 delta 会被计算为 0.2,任何在 19999.8 到 20000.2 之间的值都能通过相等性检查。这大约相当于单精度数的一个位的范围,而这已经是你能从浮点值合理获得的、几乎最紧的容差了。
那当值为零时会怎样?零——比其他浮点值更甚——可以被表示成许多不同形式。你是用 0×2⁰ 还是 0×2⁶³ 表示,都无所谓,反正都是零,对吧?幸运的是,如果你把这两个值相减,它们总会产生一个零的差,而这个差仍然落在"0 加减一个 0 的 delta"之间。所以它依然有效!
双精度浮点数使用一个小得多的乘数(1e-12),同样近似于一个位的误差。
如果你不喜欢这些范围,想放宽浮点相等断言,可以通过定义 UNITY_FLOAT_PRECISION 和 UNITY_DOUBLE_PRECISION 把它们改成任意值。例如 Unity 配置指南 中的示例:
#define UNITY_FLOAT_PRECISION 0.001f
这两个宏在 src/unity_internals.h 中均以 #ifndef 保护,因此你可以在包含 Unity 头文件之前(例如通过编译选项或 unity_config.h)覆盖默认值。浮点比较的实际执行在 src/unity.c 的 UnityAssertFloatsWithin 中完成,NaN/Inf 的检测由 UNITY_IS_NAN、UNITY_IS_INF 宏实现(默认情况下,若工具链的 math.h 未提供 isnan/isinf 宏,Unity 会自行构造;也可通过 UNITY_IS_NAN/UNITY_IS_INF 覆盖,参见 Unity 配置指南)。
如何应对非标准整数尺寸的目标?
有趣的是,C 是一门连"整数"这么基础的东西都会随目标平台而变的标准语言。根据 C 标准,int 应该是目标的自然寄存器宽度,至少 16 位,并且是字节的整数倍。标准还保证了尺寸顺序:
char <= short <= int <= long <= long long
大多数情况下 int 是 32 位的。在嵌入式世界里,int 是 16 位的情况也很多。还有极少数微控制器拥有 24 位整数,这依然是完全标准的 C。
更妙的是,有些编译器和目标平台面临两难选择:如果它们的自然寄存器宽度是 10 位或 12 位呢?显然它们无法同时满足"至少 16 位"和"匹配自然寄存器宽度"这两条要求。在这些情况下,它们常常选择自然寄存器宽度,于是我们得到类似这样的东西:
char (8 bit) <= short (12 bit) <= int (12 bit) <= long (16 bit)
嗯……有点吓人。它显然违反了一条或两条规则……但它们总得违反某些规则,于是做出了选择。
当 C99 标准出台时,引入了可选的"标准尺寸类型",也引入了用于获取整数类型 MIN/MAX 值的宏。这很棒!可惜的是,很多嵌入式编译器不能可靠地使用 C99 类型(有时是因为上面提到的怪异寄存器宽度,有时纯粹是不想支持)。
Unity 从诞生之初的一个目标就是支持每种微控制器/微处理器与 C 编译器的组合。经过多年努力,我们已非常接近这一目标。不过,如果你想在那些比较特立独行的目标上有效地使用 Unity,还是有几个技巧需要了解。
首先,为新的目标平台配置 Unity 时,需要特别注意那些自动探测类型的宏(在可用时),或者手动配置它们的宏。这两方面信息都可以在 Unity 配置指南 中找到:UNITY_EXCLUDE_STDINT_H 与 UNITY_EXCLUDE_LIMITS_H 控制自动探测;若禁用自动探测,则可用 UNITY_INT_WIDTH、UNITY_LONG_WIDTH、UNITY_POINTER_WIDTH 手动指定各类型位数,用 UNITY_SUPPORT_64 显式启用 64 位支持。
那如果你突然要面对 24 位 int 这样的怪事怎么办?最简单的方案是向上一档使用更大的尺寸。如果你的 int 是 24 位,就把 Unity 配置成使用 32 位整数;如果你的 int 是 12 位,就配置成使用 16 位。这会从两个方面影响你:
- 当 Unity 显示错误时,它会用零填充上部未使用的位。
- 你必须小心那些执行有符号运算的断言,特别是
TEST_ASSERT_INT_WITHIN。这类断言可能在错误的位置对int发生回绕,导致虚假失败。此时你可以随时退回到简单的TEST_ASSERT,自己来做这些运算。
结语与延伸阅读
断言是 Unity 的灵魂:TEST_ASSERT_EQUAL_* 系列覆盖整数、十六进制、字符、指针、字符串、内存与数组,_WITHIN 系列覆盖范围比较,_ARRAY 与 _EACH_EQUAL 系列覆盖块级内存比较,float/double 系列则提供了适合浮点本质的近似比较与特殊值(Inf/NaN)判定。配合 _MESSAGE 后缀,你可以在每个断言上附加可读的失败上下文。
想深入理解底层实现,可以直接阅读 src/unity.h(全部断言宏的公开接口)、src/unity_internals.h(内部实现与默认精度配置)与 src/unity.c(断言执行与消息输出);想要验证行为,可以浏览 test/tests/ 下的测试用例(如 test_unity_integers.c、test_unity_floats.c、test_unity_doubles.c、test_unity_arrays.c、test_unity_strings.c)。配置层面的完整选项清单,请查阅 Unity 配置指南;从零开始搭建测试工程,可参考 Unity 入门指南 与仓库中的 examples/ 示例工程。