/couttast

a lightweight header-only unit test framework

Primary LanguageC++

tinytast: 轻量级单元测试框架

只需一个头文件的超轻量级单元测试框架。因其轻量,故不名 test ,而另名 tast 。

依赖低,只用到 C++ 标准库(甚至无需 C++11 功能)。

核心功能宏与**

  • 提供一个统一的宏 COUT 用以检测与预测比较变量或表达式的值。其名来源于 std::cout,初衷即是将信息打印出来看看是啥。
  • 提供 DEF_TAST 宏定义一个测试用例函数(对象),然后用 RUN_TAST 宏自动执行所有按此定义的测试用例。且可在命令行参数指定执行特定的测试用例。
  • 也可以自由定义无参无返回值的测试函数 void() 并手动安排调用。不过若用 TAST 宏调用,可额外打一行标题信息,然后可用 TAST_RESULT 打印测试统计信息。

使用方法最简示例

只需在测试文件中包含 tinytast.hpp 头文件,然后用 DEF_TAST 定义一系列类似 void() 函数的代码块,其中可用 COUT 输出或比较感兴趣的值。

// test-file1.cpp
#include "tinytast.hpp"

DEF_TAST(test_name, "测试用例说明")
{
    COUT(sizeof(int));
    COUT(sizeof(int), 4);
    COUT(sizeof(int)==4, true);
}

main() 只要调用 RUN_TAST 宏即可。

// main.cpp
#include "tinytast.hpp"

int main(int argc, char** argv)
{
    return RUN_TAST(argc, argv);
}

将相关测试 *.cpp 源文件编译链接在一起即可运行。

另注:v0.1 简易版不支持 DEF_TAST 定义用例及 RUN_TAST 运行用例。只支持 TAST 调用普通 void() 函 数。

构建与示例

make
make example

在本仓库主目录上提供了 makefile ,直接 make 会生成 lib/bin/ 子目录。 但这不是必须的,用户可以直接只使用 include 的头文件。

有三种方法构建可执行测试程序(命令行程序),可参见 example/basiccpp-* 的几个子目录。

  1. 自己写 main 函数。可以在调用 RUN_TAST 之前作些特殊的预处理。
  2. 链接静态库 lib/libtast_main.a ,自己不用写 main 函数,只关注写测试用例。 libtast_main.a 的功能相当简单,就只为提供个 main 入口函数。
  3. 自己将测试用例编译为动态链接库,要求导出 tast_main() 函数,该函数一般会最 终调用 RUN_TAST 宏。然后用 bin/tast_drive 程序驱动编译的测试动态库, 将动态库作为第一个命令行参数,剩余参数会传给内部的测试库。
bin/tast_drive libmytast.so --list
bin/tast_drive libmytast.so [test_name_fileter]

注意加载动态库可能要导出环境变量 $LD_LIBRARY_PATH,毕竟一般不会将测试动态库 放到系统路径中。

本测试库所支持的命令行参数见后文。

宏定义详解

COUT 语句宏

顾名思义,COUT 就是将一个表达的值打印出来。支持任意类型,只要其支持 << 操 作符。

  • 单参数 COUT(expr) :打印 expr 表达式字面量及其值,使其输出信息更有可读意义,且更易找到源文件互为参照。
  • 双参数 COUT(expr, expect) :在单参数的基础上扩展,比较 exprexpect 的值,如果相等,在行尾打上 '[OK]' 的标记,否则打上 [NO] 的标记。比较失败后 也会再打印 expect 的值。表达式与预期值的类型可以不同,但要求支持 == 操作重载。
  • 浮点数三参数 COUT(expr, expect, limit):因为浮点精度原因,直接用 == 比较 浮点数易出问题,所以允许提供额外参数指定精度。

但是也应尽量避免浮点数比较,果真需要,可显式如下调用:

COUT(abs(expr-expect) <= limit, true);

单参数的 COUT 也可称为输出宏,双参数(以上)则称为断言宏。 对于断言宏还有两个扩展变种,可选使用。

  • COUTF(expr, expect): 只有当断言失败时才输出信息,用于减少成功时的输出信息。
  • COUT_ASSERT(expr, expect): 断言失败时会因 return 终止当前测试函数,可避 免因一个关键失败导致后续连续失败甚至异常。

DESC 描叙宏

另外提供两个纯输出函数,仅为改善输出信息的可读性,可选使用。

  • DESC(msg, ...):类似 printf 调用。
  • CODE(statement):执行语句前打印出该语句。

TAST 宏半自动测试

  • TAST(test_fun):调用测试函数名(void()类型的函数指针)
  • TAST_RESULT:打印测试统计信息

为不失灵活性,完全可以按常规编程习惯定义一些函数,只要这些函数用到 COUT 宏就 能输出与比较。然后手动在 main() 中组织调用这些函数。不过若是 void() 类型的 函数,则可用 TAST() 宏来调用,将会额外跟踪加入统计信息 TAST_RESULT

TAST 宏定义与自动测试

  • DEF_TAST(case_name, desc):相当于测试函数 void tast_case() 加强版,还可 以为该测试用例附加一段描叙说明。本质上是定义了一个类及其实例。随后的 {} 代 码块是该类的一个虚函数实现。
  • RUN_TAST():自动执行由 DEF_TAST 定义的所有测试用例,且按测试名的字典序依 次执行。可以传入 (argc, argv) 命令行参数,以筛选执行特定的测试用例。

在定义测试用例时,参数 case_name 要求是合法的 C++ 符号(不加双引号),desc 是 任意描叙字符串(加引号)。描叙参数也可省略,但建议加上便于人工管理测试用例。

在运行测试用例时,RUN_TAST()最后会自动调用 TAST_RESULT 宏,返回的是失败用 例个数,可直接当作 main 的返回值, 0表示测试成功。

命令行参数解析

当多个输入的命令行参数传给 RUN_TAST() 时,除特定选项外,将依次对其解析:

  • 如果参数完全匹配某个测试名,则只运行该用例。
  • 如果参数仅是单字符,则只运行以该字符开头的用例。
  • 如果参数以 ^ 开头或以星号 * 结尾,则查找特定前缀的用例。
  • 如果参数以 $ 结尾或以星号 * 开头,则查找特定后缀的用例。
  • 其他无特殊字符时,查找任意包含给定参数的用例。

这些规则应该符合直觉与方便使用。暂不打算支持完整正则表达式,以保证库的轻量化。

已知 bug (特性),多个命令参数筛选的测试用例没有互斥与唯一性。即在非用例全名 的情况下,有可能多次查找到同一个用例,重复运行。

作为选项的命令行参数,都以一个或多个 - 开头,如果选项有参数,只能用 = 分隔 选项名及其参数,即形如 --key--key=val 格式。不支持用空格分隔(否则会当 作普通的测试用例参数)。约定统一的选项参数格式也是为实现轻量化考虑。

目前支持的选项有以下几个:

  • --help : 输出简要帮助信息,可用选项。
  • --list :列出所有测试用例名,而不实际运行测试。
  • --List :列出所有测试用例名及其描叙,即 DEF_TAST 宏定义时的两个参数。
  • --cout=fail :只输出 COUT 断言失败的语句,相当于 COUTF 效果。
  • --cout=silent :与 fail 类似,但输出的信息还更精简,单参数 COUT 输出宏 与纯描叙性的 DESC 宏也被沉默,不再有输出。--cout 只能取这两个值有意义, 其他值都等效于 all 表示默认输出全部信息。

另注,--list--help 一样是短路的,输出信息后直接退出,因此也会忽略后面 提供的测试名筛选参数,而是列出所有测试名。如需查询特定测试名,可管道至 grep 或其他更过滤工具。

./tast_program --list | grep something

diff 模拟回归测试

./tast_program > tast.cout
# after some time for fix ...
mv tast.cout tast.bout
./tast_program > tast.cout
diff tast.cout tast.bout

可将不同时段的测试输出保存文件,用 diff 比较法模拟回归测试。不过此法要注 意的是测试中不宜输出指针地址,因为不同运行时内存地址不可能做到保持一致。另外, 如果测试代码中只用到双参数 COUT 测试,一般看统计结果全部通过即可。diff 比较 法更适用于测试代码中大量使用单参数 COUT 不能自动判断通过与否的情况。