C++实用技巧--单元测试实现

作者在 2010-08-17 01:48:58 发布以下内容
复杂的东西写多了,如今写点简单的好了。由于功能上的需要,Vczh Library++3.0被我 搞得很离谱。为了开发维护的遍历、减少粗心犯下的错误以及增强单元测试、回归测试和测试工具,因此记录下一些开发上的小技巧,以便抛砖引玉,造福他人。欢 迎高手来喷,菜鸟膜拜。

    之前的文章讲了指针和内存的一些问题,今天说一下单元测试的问题。如果在团队里面没有对单元测试的框架有要求的 话,其实我们可以使用一个最简单的方法来搭建在IDE里面运行的单元测试框架,整个框架只需十几行代码。我们先来考虑一下功能最少的单元测试框架需要完成 什么样的内容。首先我们要运行一个一个的测试用例,其次在一个测试用例里面我们要检查一些条件是否成立。举个例子,我们写一个函数将两个字符串连接起来, 一般来说要进行下面的测试:
 1 #include "MyUnitTestFramework.h"//等一下我们会展示一下如何用最少的代码完成这 个头文件的内容
 2 #include ""
 3 
 4 TEST_CASE(StringConcat)
 5 {
 6   TEST_ASSERT(concat("a""b")=="ab");
 7   TEST_ASSERT(concat("a""")=="a");
 8   TEST_ASSERT(concat("""b")=="b");
 9   TEST_ASSERT(concat("""")=="");
10   .
11 }
12 
13 int wmain()
14 {
15   return 0;
16 }

    如果我们的单元测试框架可以这么写,那显然做起什么事情来都会方便很多,而且不需要向一些其他的测试框架一样注册一大堆东西,或者是写一大堆配置函数。当 然这次我们只做功能最少的测试框架,这个框架除了运行测试以外,不会有其他功能,譬如选择哪些测试可以运行啦,还是在出错的时候log一些什么啦之类。之 所以要在IDE里面运行,是因为我们如果做到TEST_ASSERT中出现false的话,立刻在该行崩溃,那么IDE就会帮你定位到出错的 TEST_ASSERT中去,然后给你显示所有的上下文信息,譬如说callstack啦什么的。友好的工具不用简直对不起自己啊,干吗非得把单元测试做 得那么复杂捏,凡是单元测试,总是要全部运行通过才能提交代码的。

    那么我们来看看上面的单元测试的代码。首先写了TEST_CASE的那个地方,大括号里面的代码会自动运行。其次TEST_ASSERT会在表达式是 false的时候崩溃。先从简单的入手吧。如何制造崩溃呢?最简单的办法就是抛异常:
1 #define TEST_ASSERT(e) do(if(! (e))throw "今晚没饭吃。";}while(0)

    这里面有两个要注意的地方。首先e要加上小括号,不然取反操作符就有可能做出错误的行为。譬如说当e是a+b==c的时候,加了小括号就变成if(! (a+b==c))...,没有加小括号就变成if(!a+b==c)...,意思就完全变了。第二个主意的地方是我使用do{...}while(0) 把语句包围起来了。这样做的好处是可以在任何时候TEST_ASSERT(e)都像一个语句。譬如我们可能这么写:
1 if(a)
2   TEST_ASSERT(x1);
3 else if(b)
4 {
5   TEST_ASSERT(x2);
6   TEST_ASSERT(x3);
7 }

    如果没有do{...}while(0)包围起来,这个else就会被绑定到宏里面的那个if,你的代码就被偷偷改掉了。

    那么现在剩下TEST_CASE(x){y}了。什么东西可以在main函数外面自动运行呢?这个我想熟悉C++的人都会知道,就是全局变量的构造函数 啦。所以TEST_CASE(x){y}那个大括号里面的y只能在全局变量的构造函数里面调用。但是我们知道写一个类的时候,构造函数的大括号写完了,后 面还有类的大括号,全局变量的名称,和最终的一个分号。为了把这些去掉,那么显然{y}应该属于一个普通的函数。那么全局变量如何能够使用这个函数呢?方 法很简单,把函数前置声明一下就行了:
 1 #define TEST_CASE(NAME)                                            \
 2         extern void TESTCASE_##NAME();                             \
 3         namespace vl_unittest_executors                            \
 4         {                                                          \
 5             class TESTCASE_RUNNER_##NAME                           \
 6             {                                                      \
 7             public:                                                \
 8                 TESTCASE_RUNNER_##NAME()                           \
 9                 {                                                  \
10                     TESTCASE_##NAME();                             \
11                 }                                                  \
12             } TESTCASE_RUNNER_##NAME##_INSTANCE;                   \
13         }                                                          \
14         void TESTCASE_##NAME()

    那我们来看看TEST_CASE(x){y}究竟会被翻译成什么代码:
 1 extern void TESTCASE_x();
 2 namespace vl_unittest_executors
 3 {
 4     class TESTCASE_RUNNER_x
 5     {
 6     public:
 7         TESTCASE_RUNNER_x()
 8         {
 9             TESTCASE_x();
10         }
11     } TESTCASE_RUNNER_x_INSTANCE;
12 }
13 void TESTCASE_x(){y}

    到了这里是不是很清楚了捏,首先在main函数运行之前TESTCASE_RUNNER_x_INSTANCE变量会初始化,然后调用 TESTCASE_RUNNER_x的构造函数,最后运行函数TESTCASE_x,该函数的内容显然就是{y}了。这里还能学到宏是如何连接两个名字成 为一个名字,和如何写多行的宏的。

    于是MyUnittestFramework.h就包含这两个宏,其他啥都没有,是不是很方便呢?打开Visual C++,建立一个工程,引用这个头文件,然后写你的单元测试,最后F5就运行了,多方便啊,啊哈哈哈。

    这里需要注意一点,那些单元测试的顺序是不受到保证的,特别是你使用了多个cpp文件的情况下。于是你在使用这个测试框架的同时,会被迫保证执行一次单元 测试不会对你的全局状态带来什么副作用,以便两个测试用例交换顺序执行的时候仍然能稳定地产生相同的结果。这对你写单元测试有帮助,而且为了让你的代码能 够被这么测试,你的代码也会写的有条理,不会依赖全局状态,真是一举两得也。而且说不定单元测试用例比你的全局变量的初始化还先执行呢,因此为了使用这个 测试框架,你将会不得不把你的全局变量隐藏在一个cpp里面,而暴露出随时可以被调用的一组函数出来。这样也可以让你的代码在使用全局状态的时候更加安 全。

    今天就讲到这里了。下一篇要写什么我还没想好,到时候再说吧。
技术 | 阅读 2062 次
文章评论,共1条
vfdff(作者)
2010-09-07 00:58
1
C++单元测试框架的比较<br />
 单元测试现 在已经成为标准的编程实践,但是C++缺少Java和.Net平台语言的反射机制,所以无法枚举测试方法, 必须手工添加,或者使用一些特别的宏,弄得代码非常难看。Java语言单元测试是JUnit的 天下,C#基本上都用NUnit,而C++则群花怒放,单元测试框架非常多,JUnit移植过来的 CppUnit,Boost::test,CppTest,CxxTest, TUT等等。但是解决方案最好的是CxxTest和TUT,CxxTest采用的方法比较特殊,用Perl分析C++的源文件,从中抽取测试方法,创建 TestSuite。语法与JUnit非常相似,没有使用高级的C++特性,也没有定义特别的宏,无须写额外的代码。TUT也是一个不错的解决方案,利用 高级C++ Template功能,必须比较新的编译器才支持,比如VC6和VS.NET 2002就不支持,必须VS.NET 2003以上或者Intel C++ Complier 8.1以上。<br />
  1、 TUT<br />
  结构框架简单。添加新的测试工作量小;无须注册测 试;可移植性好(因其只需两个头文件,就可以完成测试工作);便于装卸;提供接口可以扩展其输出方式等。<br />
  最大的优点:轻量级,便于装卸 和可扩展其输出方式;<br />
  缺点:断言似乎不是很好,只用了一个ensure()函数,不知道对复杂的测试是否支持;输出的测试结果较为简 单。<br />
  2、 Boost::test<br />
  结构框架较为复杂。添加新的测试工作量也不大;提供多种测试方法,可注册测试用 例,也可不注册;可移植性一般;装卸不易;在控制异常、崩溃方面的能力胜过其 它所有对手;拥有良好的断言功能;大概能支持多种输出方式,但更改输出方式不易;支持测试套件。<br />
  最 大的优点:控制异常崩溃的能力、良好的断言、输出结果较为详细、编写测试的方法灵活;<br />
  缺点:结构框架较为复杂,更改输出方式不易,装卸 不易。<br />
  3、 CXXTest<br />
  结构框架的复杂性处于TUT与boost::test之间。添加新的测试工作量非常小; 无须注册测试用例;可移植性很好;便于装卸;控制异常、崩溃方面的能力也不错;拥有良好的断言功能;支持多种输出方式;支持测试套件。<br />
   最大的优点:编译即测试方式,并且可以双击结果行立即定位到相应的源代码,相当吸引人;支持多种输出,输出结果较为详细;编写测试简单;<br />
   缺点:需要用到perl对测试代码进行文法扫描,生成可执行代码,需要用到makefile文件(不是必须);准备工作比较麻烦。
游客请输入验证码
浏览1970425次