一个simple但是够用的单元测试框架.

在项目开发过程中,单元测试必不可少,cpp中常用的单元测试框架有boost.test, gtest等,以boost为例,boost的单元测试框架的一般用法为:

test.cpp:

other.cpp:

实际上我们可以有无穷多个.cpp,里面使用BOOST_AUTO_TEST_CASE去做具体的测试,而不用关心main函数在哪里,错误具体如何统计.

除了这样的常用用法之外,boost还支持测试套件测试模块等大量功能,这些功能在很多时候都没用被用到,正是这些多数时间都没用的功能使得boost的测试框架相当庞大(9841行代码), 严重拖慢编译速度,实际上在日常的测试中有REQUIRE和CHECK的语义就够用了。

boost的这个测试框架还有一个严重的问题,正如注释中所言,它使用异常来中断这是用例,那么如果我们有如下代码:

所以我们现在的需求就是:

1.满足常用的功能(REQUIRE, CHECK, 用户自定义出错时的行为).

2.足够简洁.

3.在任何情况下REQUIRE失败都终止test case.

下面就为大家带来不到200行的header only的一个mini测试框架.

首先它的用法和BOOST差不多:

test.cc:

other.cc:

有木有很棒!然后更棒的是它是一个只有187行的头文件。下面分享一下大概的实现原理。

首先我们需要有一个全局的对象用来记录所有用户通过TEST_CASE宏注册的测试用例,这个全局对象当然只能用函数的局部静态变量来储存,因为如果用类的静态成员的话必须要在cpp里有定义,而直接放在头文件里则会导致多次定义的问题。这个全局对象的定义的简化版本如下:

 

然后我们需要解决用户通过宏TEST_CASE注册测试用例的问题,实际上TEST_CASE的作用是定义一个函数,但是这里有一个问题是,怎么通过这个宏将其定义的函数注册到全局UnitTest中?比如:

这里的TEST_CASE如何将test_func这个函数的地址传给全局对象供以后执行?一般的函数是没法在全局执行的,但是总有些东西是例外,构造函数就是其中的一个,于是我们可以通过构造函数来实现test_func的传递:

其中TestCase构造函数的定义如下:

于是TEST_CASE的工作流程就是, 先声明test_func, 然后定义一个TestCase并将test_func的地址传递过去,然后写出void test_func()让用户自己通过一对{}写出函数体.

另一个要解决的问题是REQUIRE失败的时候怎么终止当前的用例。由于用户的代码可能会捕获异常,所以不能通过异常来实现,在C里实现跳转的常用方式是longjmp,我们也不妨一试:

 

这样,如果REQUIRE失败,那么setjmp就会返回1, 然后continue语句会直接跳到下一个test case执行。

当然通过longjmp方式来跳过测试的办法存在一定争议,因为在C++中longjmp可能跳过非trivial的析构函数,这种情况是UB,但是这里的应用场景比较特殊—–用在单元测试且是致命失败的情况下。目前的想法是在未来的版本中用一个宏来让用户选择是抛异常还是longjmp。

第一次写技术文章,有不清处或不足之处欢迎指出。

详细代码: https://github.com/lucklove/ZBase/blob/master/inc/UnitTest.hh

发表评论

Copy Protected by Chetan's WP-Copyprotect.