現在市面上已經有很多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) 閱讀(2527)
評論(4) 編輯 收藏 引用 所屬分類:
其他