打造自己的Unit Test工具

打造自己的Unit Test工具
    现在市面上已经有很多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  }

    让我们看看结果吧!
打造自己的Unit Test工具_第1张图片

    这里贴出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了。

你可能感兴趣的:(打造自己的Unit Test工具)