【转】C++实用技巧(四)

 之前的文章讲了指针和内存的一些问题,今天说一下单元测试的问题。如果在团队里面没有对单元测试的框架有要求的话,其实我们可以使用一个最简单的方法来搭建在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里面,而暴露出随时可以被调用的一组函数出来。这样也可以让你的代码在使用全局状态的时候更加安全。

posted on 2011-07-31 13:04  xuangong  阅读(179)  评论(0编辑  收藏  举报

导航