现在市面上已经有很多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) 阅读(2512)
评论(4) 编辑 收藏 引用 所属分类:
其他