随笔-341  评论-2670  文章-0  trackbacks-0
    现在市面上已经有很多Unit Test的工具了。对于C++来说最为著名的莫过于CppUnit。CppUnit已经具有丰富的功能,例如UI、报告生成等等。那么为什么还要自己做Unit Test工具呢?主要还是为了学习,其次是可以为自己的特殊需求打造特殊的工具。

    随着程序越来越复杂,Unit Test的重要作用已经不言而喻了。在Kernel FP的开发过程中,由于经常需要重构,于是Unit Test就已经成为必不可少的工具之一了。接口不变化的重构,可以通过足够的Unit Test来在最大的程度上保证新的代码不会引发更多问题。为了开发Unit Test的工具,我们首先了解一下一个Unit Test的典型需求是什么。

    Unit Test作为一种检验代码是否有已经预期错误的手段,我们需要很多的TestCase,每一个TestCase又需要若干的面向具体问题的检查,称为TestMethod,每一个TestMethod又需要若干的条件判断。我们可以在Unit Test当中写很多返回bool类型的表达式,并且让true代表成功,false代表失败。于是Unit Test的一个重要功能就是执行我们定义的所有条件表达式,并作出统计。

    Unit Test的输入可以有很多,但是测试代码是必不可少的。于是我们需要一些容器来包含代码,并且让一个处于后台的执行引擎来依次执行我们的代码然后输出信息。

    Unit Test的输出无非就是Unit Test的执行状态,也就是每一个条件表达式所在的位置、结果以及附带的供我们查看的文字信息。那么应该如何处理输出信息呢?在自己开发Unit Test框架的过程中,我们可以将信息进行分类,然后使用Observer模式将信息传播出去:
 1         class IVL_TestRecorder : public IVL_Interface
 2         {
 3         public:
 4             virtual void                    BeginRun(VL_TestRunner* Runner)=0;
 5             virtual void                    EndRun()=0;
 6             virtual void                    BeginCase(VInt Index)=0;
 7             virtual void                    EndCase()=0;
 8             virtual void                    BeginMethod(VInt Index)=0;
 9             virtual void                    EndMethod()=0;
10             virtual void                    IgnoreMethod(VInt Index)=0;
11             virtual void                    ExceptionOccur()=0;
12             virtual void                    Pass(VUnicodeString Message)=0;
13             virtual void                    Fail(VUnicodeString Message)=0;
14             virtual void                    Print(VUnicodeString Message)=0;
15         };

    从上面的接口我们可以看到,每一步的开始和结束,以及条件的信息以及结构都期望输入到一个IVL_TestRecorder对象里面。这里我们可以做很多事情。首先VL_TestRunner可以,而且必须可以获得用于分辨每一个TestMethod的信息,譬如说TestCase的名字以及TestMethod的名字。其次,无论TestMethod里面做了什么样的事情,VL_TestRunner都应当尽可能地捕捉到。

    在IVL_TestRecorder的帮助之下我们就可以实现一些工具了。譬如一个用于将信息传播给一堆Recorder的Recorder,譬如一个用于生成HTML报告的Recorder,譬如一个用于统计的Recorder,譬如一个将信息发布到GUI上的Recorder等等。第一种Recorder将Recorder变成了树,然后其余的Recorder只需要装饰叶节点,就可以获得一个可扩展性非常强的Recorder工具了。

    当然,接下去的问题是如何包含测试代码。我们可以将很多VL_TestCase的实例装入VL_TestRunner,并且将很多函数装入VL_TestCase。这个时候boost的functor就可以大大简化这个过程,当然我自己并没有使用它,而是自己写了一个类似的工具。于是VL_TestRunner的职责就是调用VL_TestCase,VL_TestCase的职责就是调用TestMethod,而TestMethod则是我们写的测试代码,职责是测试一系列的条件并输出信息。通过两个类以及一些宏的配合,我们最终可以得到类似于下面的结果:
 1 class TestCase1 : public VL_TestCase
 2 {
 3 public:
 4     TestCase1()
 5     {
 6         VL_UNITTEST_ADDMETHOD(TestCase1,TestMethod1);
 7         VL_UNITTEST_ADDMETHOD(TestCase1,TestMethod2);
 8         VL_UNITTEST_ADDMETHOD(TestCase1,TestMethod3);
 9     }
10 
11     void TestMethod1()
12     {
13         VL_UNITTEST_CHECK(1==1);
14         VL_UNITTEST_CHECK(1==0);
15     }
16 
17     void TestMethod2()
18     {
19         VL_UNITTEST_ASSERT(1==1);
20         VL_UNITTEST_PRINT(L"MESSAGE");
21     }
22 
23     void TestMethod3()
24     {
25         VL_UNITTEST_ASSERT(1==0);
26         VL_UNITTEST_PRINT(L"MESSAGE");
27     }
28 };
29 
30 class TestCase2 : public VL_TestCase
31 {
32 public:
33     TestCase2()
34     {
35         VL_UNITTEST_ADDMETHOD(TestCase2,TestMethod1);
36         VL_UNITTEST_ADDMETHOD(TestCase2,TestMethod2);
37     }
38 
39     void TestMethod1()
40     {
41         int a=0;
42         a=a/a;
43         VL_UNITTEST_PRINT(L"REACH");
44     }
45 
46     void TestMethod2()
47     {
48         throw "XXX";
49         VL_UNITTEST_PRINT(L"REACH");
50     }
51 };
52 
53 class TestRunner : public VL_TestRunner
54 {
55 public:
56     TestRunner()
57     {
58         VL_UNITTEST_ADDCASE(TestCase1);
59         VL_UNITTEST_ADDCASE(TestCase2);
60     }
61 };


    上面的宏都是一些很简单的技巧,就不详细讲了。如果熟悉宏的语法的话,则可以很容易的实现它们。至于数据结构上,VL_TestRunner可以看成一个VL_TestCase的数组,而VL_TestCase则包含了一个函数指针的数组。

    那这两个框架类的接口应当是什么样子的呢?我们允许Runner在内部添加TestCase,允许TestCase在内部添加TestMethod,并且允许从Runner获得整个结构的内容,最后Runner应当能够执行所有的TestMethod,于是接口很自然地就会变成这个样子:

 1 class VL_TestCase : public VL_Base
 2 {
 3     friend class VL_TestRunner;
 4 private:
 5     VL_TestMethodMap                FMethods;
 6     IVL_TestRecorder*                FCurrentRecorder;
 7 
 8     VBool                            RunCPPEHProtected(IVL_TestRecorder* Recorder , VL_TestMethod* Method);
 9     VBool                            RunSEHProtected(IVL_TestRecorder* Recorder , VL_TestMethod* Method);
10     void                            Run(IVL_TestRecorder* Recorder , VL_TestRunner* Runner , VInt CaseIndex , IVL_TestFilter* Filter);
11 
12 protected:
13     virtual void                    Initialize();
14     virtual void                    Finalize();
15 
16     void                            AddMethod(VUnicodeString Name , VL_TestMethodPtr Method);
17     IVL_TestRecorder*                GetCurrentRecorder();
18 
19 public:
20     VL_TestCase();
21 
22     VInt                            GetMethodCount();
23     VUnicodeString                    GetMethodName(VInt Index);
24 };
25 typedef VL_AutoPtr<VL_TestCase>                                    VL_TestCasePtr;
26 typedef VL_List<VL_TestCasePtr , false , VL_TestCase*>            VL_TestCaseList;
27 typedef VL_ListedMap<VUnicodeString , VL_TestCasePtr>            VL_TestCaseMap;
28 
29 class VL_TestRunner : public VL_Base
30 {
31 protected:
32     VL_TestCaseMap                    FCases;
33     void                            AddCase(VUnicodeString Name , VL_TestCase* Case);
34 
35 public:
36     VL_TestRunner();
37 
38     VInt                            GetCaseCount();
39     VUnicodeString                    GetCaseName(VInt Index);
40     VL_TestCase*                    GetCase(VInt Index);
41     VL_TestCase*                    GetCase(VUnicodeString Name);
42     void                            Run(IVL_TestRecorder* Recorder , IVL_TestFilter* Filter);
43 };

    通过private的run加上一个friend class VL_TestRunner的声明,还有处于protected内的若干函数,我们将每一个函数的可视范围都清晰地定义了下来。好了,有了Recorder和Runner,我们就可以从外部监视Unit Test的执行状态,并且不需要修改任何代码就能进行扩展。因此我们可以很简单的写一个Console Host来执行Unit Test,也可以写一个GUI Host来执行Unit Test。今天由于时间的关系,我只实现了一个简单的Console Host,并且让这个Console Host自己将Recorder交给Runner进行监视。这可以使得main函数非常简单地完成:
1 void vlmain()
2 {
3     GetConsole()->SetPauseOnExit(true);
4     GetConsole()->SetTestMemoryLeaks(true);
5     GetConsole()->SetTitle(L"Vczh Library++ 2.0 Unit Test Framework");
6 
7     TestRunner Runner;
8     SimpleConsoleTestHost(&Runner);
9 }

    其中,SimpleConsoleTestHost的内容如下:
 1 void SimpleConsoleTestHost(VL_TestRunner* Runner , IVL_TestRecorder* Recorder)
 2 {
 3     VL_TestRecorderList RecorderList;
 4     VL_TestConsoleRecorder ConsoleRecorder;
 5     VL_TestAllAcceptFilter AllAcceptFilter;
 6 
 7     if(Recorder)
 8     {
 9         RecorderList.Add(Recorder);
10     }
11     RecorderList.Add(&ConsoleRecorder);
12     Runner->Run(&RecorderList,&AllAcceptFilter);
13 }

    让我们看看结果吧!

    这里贴出VL_UNITTEST_*的内容:
 1 #define VL_UNITTEST_ADDCASE(CLASSNAME)                                \
 2     do{                                                                \
 3         AddCase(L#CLASSNAME,(new CLASSNAME()));}                    \
 4     while(0)
 5 
 6 #define VL_UNITTEST_ADDMETHOD(CLASSNAME,METHODNAME)                    \
 7     do{                                                                \
 8         VL_TestMethod* Method=new VL_TestMethod;                    \
 9         Method->Bind(this,&CLASSNAME::METHODNAME);                    \
10         this->AddMethod(L#METHODNAME,Method);                        \
11     }while(0)
12 
13 #define VL_UNITTEST_PRINT(MESSAGE)                                    \
14     do{                                                                \
15         GetCurrentRecorder()->Print(MESSAGE);                        \
16     }while(0)
17 
18 #define VL_UNITTEST_TEST(CONDITION,MESSAGE,ISASSERT)                \
19     do{                                                                \
20         if(CONDITION)                                                \
21         {                                                            \
22             GetCurrentRecorder()->Pass(MESSAGE);                    \
23         }                                                            \
24         else                                                        \
25         {                                                            \
26             GetCurrentRecorder()->Fail(MESSAGE);                    \
27             if(ISASSERT)                                            \
28             {                                                        \
29                 return;                                                \
30             }                                                        \
31         }                                                            \
32     }while(0)
33 
34 #define VL_UNITTEST_CHECK_MESSAGE(CONDITION,MESSAGE)                \
35     VL_UNITTEST_TEST(CONDITION,MESSAGE,false)
36 
37 #define VL_UNITTEST_CHECK(CONDITION)                                \
38     VL_UNITTEST_CHECK_MESSAGE(CONDITION,L#CONDITION)
39 
40 #define VL_UNITTEST_CHECK_FAIL(MESSAGE)                                \
41     VL_UNITTEST_CHECK_MESSAGE(false,MESSAGE)
42 
43 #define VL_UNITTEST_ASSERT_MESSAGE(CONDITION,MESSAGE)                \
44     VL_UNITTEST_TEST(CONDITION,MESSAGE,true)
45 
46 #define VL_UNITTEST_ASSERT(CONDITION)                                \
47     VL_UNITTEST_ASSERT_MESSAGE(CONDITION,L#CONDITION)
48 
49 #define VL_UNITTEST_ASSERT_FAIL(MESSAGE)                            \
50     VL_UNITTEST_ASSERT_MESSAGE(false,MESSAGE)

    接下来就是写一些Test、完成GUI Host、完成Kernel FP了。
posted on 2008-11-13 09:38 陈梓瀚(vczh) 阅读(2495) 评论(4)  编辑 收藏 引用 所属分类: 其他

评论:
# re: 打造自己的Unit Test工具 2008-11-13 16:50 | LOGOS
轮子,虽然是以学习为目的

使用太繁琐,我现在手头上使用的非常方便
TEST( testa )
{
}

TEST_F( fixture , testb )
{
}  回复  更多评论
  
# re: 打造自己的Unit Test工具 2008-11-13 18:45 | 陈梓瀚(vczh)
你那个需要全局变量。而且host是自动的话,那么等于失去了添加功能的机会。除了修改测试的代码。  回复  更多评论
  
# re: 打造自己的Unit Test工具 2008-11-14 04:19 | 空明流转
相比之下我还是个MACRO控,以前我也写过UnitTest的东东。。。  回复  更多评论
  
# re: 打造自己的Unit Test工具 2008-11-17 04:33 | Lnn
不错哎  回复  更多评论
  

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理