一、前言
測試驅動開發(TDD)是以測試作為開發過程的中心,它堅持,在編寫實際 代碼之前,先寫好基於產品代碼的測試代碼。開發過程的目標就是首先使測試能夠通過,然 後再優化設計結構。測試驅動開發式是極限編程的重要組成部分。XUnit,一個基於測試驅動 開發的測試框架,它為我們在開發過程中使用測試驅動開發提供了一個方便的工具,使我們 得以快速的進行單元測試。XUnit的成員有很多,如JUnit,PythonUnit等。今天給大家介紹 的CppUnit即是XUnit家族中的一員,它是一個專門面向C++的測試框架。
本文不對 CppUnit源碼做詳細的介紹,而只是對CppUnit的應用作一些介紹。你將看到:
CppUnit源代碼的各個組成部分;
怎樣設置你的開發環境以能夠使用CppUnit ;
怎樣為你的產品代碼添加測試代碼(實際上應該反過來,為測試代碼添加產品代碼 。在TDD中,先有測試代碼後有產品代碼),並通過CppUnit來進行測試;
本文敘述背 景為:CppUnit1.12.0, Visual C++ 6.0, WindowsXP。文中敘述有誤之處,敬請批評指正。
一. CppUnit的安裝
從http://sourceforge.net/projects/cppunit CppUnit 的源碼包. CppUnit是開源產品 , 當前最高版本為1.12.0. (在上面的鏈接所指向的頁面上選 擇 Development Snapshot ).
下載後,將源碼包解壓縮到本地硬盤,例如解壓到E:\cppunit-1.12.0。筆者把文件夾名稱中的版本號去掉,即源碼包解壓縮到E:\cppunit。下載解 壓後,你將看到如下文件夾:
主要的文件 夾有:
doc: CppUnit的說明文檔。另外,代碼的根目錄,還有三個說明文檔,分別是 INSTALL,INSTALL-unix,INSTALL-WIN32.txt;
examples: CpppUnit提供的例子,也 是對CppUnit自身的測試,通過它可以學習如何使用CppUnit測試框架進行開發;
include: CppUnit頭文件;
src: CppUnit源代碼目錄;
config:配置 文件;
contrib:contribution,其他人貢獻的外圍代碼;
lib:存放編譯好 的庫;
src:源文件,以及編譯庫的project等;
接下來進行編譯工作。 在 src/目錄下, 將CppUnitLibraries.dsw工程文件用vc 打開。執行build/batch build,編譯 成功的話,生成的庫文件將被拷貝到lib目錄下。中途或者會有些project編譯失敗,一般不 用管它,我們重點看的是cppunit和TestRunner 這兩個project的debug和release版本。
編譯通過以後, 在lib/目錄下,會生成若干lib,和dll文件, 都以cppunit開頭. cppunitd表示debug版, cppunit表示release版。
CppUnit為我們提供了兩套框架庫, 一個為靜態的lib,一個為動態的dll。cppunit project:靜態lib;cppunit_dll project: 動態dll和lib。在開發中我們可以根據實際情況作出選擇。
你也可以根據需要選擇所 需的項目進行編譯,其中項目cppunit為靜態庫,cppunit_dll為動態庫,生成的庫文件為:
cppunit.lib:靜態庫release版;
cppunitd.lib:靜態庫debug版;
cppunit_dll.lib:動態庫release版;
cppunitd_dll.lib:動態庫debug版;
另外一個需要關注的project是TestRunner,它輸出一個dll,提供了一個基於GUI 方 式的測試環境,在CppUnit下, 可以選擇控制台方式和GUI方式兩種表現方案。兩種方案分別 如下圖所示:
我們選擇GUI 方式,所以我們也需要編譯這個project,輸出位置亦為lib文件夾。
要使用CppUnit ,還得設置好頭文件和庫文件路徑,以VC6為例,選擇Tools/Options/Directories,在 Include files和Library files中分別添加%CppUnitPath%\include和%CppUnitPath%\lib,其 中%CppUnitPath%表示CppUnit所在路徑。本文這裡分別填的是E:\CPPUNIT\INCLUDE和E:\CPPUNIT\LIB。
二、概念
在使用之前,我們有必要認識一下CppUnit中的主要類,當然你也可以先看後面的例 子,遇到問題再回過頭來看這一節。
CppUnit核心內容主要包括一些關鍵類:
Test:所有測試對象的基類。
CppUnit采用樹形結構來組織管理測試對 象(類似於目錄樹,如下圖所示),因此這裡采用了組合設計模式(Composite Pattern), Test的兩個直接子類TestLeaf和TestComposite分別表示“測試樹”中的葉節點和 非葉節點,其中TestComposite主要起組織管理的作用,就像目錄樹中的文件夾,而TestLeaf 才是最終具有執行能力的測試對象,就像目錄樹中的文件。
Test最重要 的一個公共接口為:
virtual void run(TestResult *result) = 0;
其 作用為執行測試對象,將結果提交給result。
在實際應用中,我們一般不會直接使用 Test、TestComposite以及TestLeaf,除非我們要重新定制某些機制。
TestFixture:用於維護一組測試用例的上下文環境。
在實際應用中, 我們經常會開發一組測試用例來對某個類的接口加以測試,而這些測試用例很可能具有相同 的初始化和清理代碼。為此,CppUnit引入TestFixture來實現這一機制。
TestFixture具有以下兩個接口,分別用於處理測試環境的初始化與清理工作:
virtual void setUp();
virtual void tearDown();
TestCase:測試 用例,從名字上就可以看出來,它便是單元測試的執行對象。
TestCase從Test和 TestFixture多繼承而來,通過把Test::run制定成模板函數(Template Method)而將兩個父 類的操作融合在一起,run函數的偽定義如下:
// 偽代碼
void TestCase::run(TestResult* result)
{
result->startTest(this); // 通知result測試開始
if( result->protect(this, &TestCase::setUp) ) // 調用setUp,初始化環境
result->protect(this, &TestCase::runTest); // 執行runTest,即真正的測試代碼
result- >protect(this, &TestCase::tearDown); // 調用tearDown,清理環境
result->endTest(this); // 通知result測試結束
}
這裡要提到的是函數 runTest,它是TestCase定義的一個接口,原型如下:
virtual void runTest ();
用戶需從TestCase派生出子類並實現runTest以開發自己所需的測試用例。
另外還要提到的就是TestResult的protect方法,其作用是對執行函數(實際上是函 數對象)的錯誤信息(包括斷言和異常等)進行捕獲,從而實現對測試結果的統計。
TestSuit:測試包,按照樹形結構管理測試用例
TestSuit是 TestComposite的一個實現,它采用vector來管理子測試對象(Test),從而形成遞歸的樹形 結構。
TestFactory:測試工廠
這是一個輔助類,通過借助一系列宏定 義讓測試用例的組織管理變得自動化。參見後面的例子。
TestRunner:用於執行 測試用例
TestRunner將待執行的測試對象管理起來,然後供用戶調用。其接口為 :
virtual void addTest( Test *test );
virtual void run( TestResult &controller, const std::string &testPath = "" );
這也 是一個輔助類,需注意的是,通過addTest添加到TestRunner中的測試對象必須是通過new動 態創建的,用戶不能刪除這個對象,因為TestRunner將自行管理測試對象的生命期。
三、CppUnit 的使用
以上工作完成以後,就可以正式使用CppUnit了,由於單元測試是 TDD(測試驅動開發)的利器,一般人會先寫測試代碼,然後再寫產品代碼,不過筆者認為先 寫產品代碼框架後再寫測試代碼,然後通過慢慢補充產品代碼以使得能通過測試的方法會好 些。不管先寫誰只要寫得舒服安全就可以。本文決定先寫測試代碼。
前面我們提到過 ,CppUnit最小的測試單位是TestCase,多個相關TestCase組成一個TestSuite。要添加測試 代碼最簡單的方法就是利用CppUnit為我們提供的幾個宏來進行(當然還有其他的手工加入方 法,但均是殊途同歸,大家可以查閱CppUnit頭文件中的演示代碼)。這幾個宏是:
CPPUNIT_TEST_SUITE() 開始創建一個TestSuite;
CPPUNIT_TEST() 添加 TestCase;
CPPUNIT_TEST_SUITE_END() 結束創建TestSuite;
CPPUNIT_TEST_SUITE_NAMED_REGISTRATION() 添加一個TestSuite到一個指定的 TestFactoryRegistry工廠。
感興趣的朋友可以在HelperMacros.h看看這幾個宏的 聲明,本文在此不做詳述。
假定我們要實現一個類,類名暫且取做CPlus,它的功能 主要是實現兩個數相加(多簡單的一個類啊,這也要測試嗎?不要緊,我們只是了解怎樣加 入測試代碼來測試它就行了,所以越簡單越好)。 假定這個類要實現的相加的方法 是:
int Add(int nNum1, int nNum2);
OK,那我們先來寫測試這個方 法的代碼吧。TDD 可是先寫測試代碼,後寫產品代碼(CPlus)的哦!先寫的測試代碼往往是不 能運行或編譯的,我們的目標是在寫好測試代碼後寫產品代碼,使之編譯通過,然後再進行 重構。這就是Kent Beck說的“red/green/refactor”。所以,上面的類名和方法 應該還只是在你的心裡,還只是你的idea而已。
根據測試驅動的原理,我們需要先建 立一個單元測試框架。我們在VC中為測試代碼建立一個project。通常,測試代碼和被測試對 象(產品代碼)是處於不同的project中的。這樣就不會讓你的產品代碼被測試代碼所 “污染 ”。
由於在CppUnit下, 可以選擇控制台方式和UI方式兩種表現方 案,我們選擇UI方式。在本例中,我們將建立一個基於GUI 方式的測試環境。因此我們建立 一個基於對話框的Project。假設名為UnitTest。
建立了UnitTest project之後,我 們首先配置這個工程。
首先在project中打開RTTI開關,具體位置在菜單 Project/Settings/C++/C++ Language。如下圖所示設置:
由於CppUnit 所用的動態運行期庫均為多線程動態庫,因此你的單元測試程序也得使用相應設置,否則會 發生沖突。於是我們在Project/Settings/C++/Code Generation中進行如下設置:
在 Use run-time library一欄中,針對debug和release分別設置為‘Debug Multithreaded DLL’和‘Multithreaded DLL’。如下圖所示:
最後別忘了在project中link正確的lib。包括本例采用的cppunit.lib和 cppunitd.lib靜態庫以及用於GUI方式的TestRunner.dll對應的lib。具體位置在 Project/Settings/Link/General
在‘Object/library modules’中,針 對debug和release分別加入cppunitd.lib testrunnerd.lib和cppunit.lib TestRunner.lib 。如下圖所示:
最後,由於 TestRunner.dll為我們提供了基於GUI的測試環境。為了讓我們的測試程序能正確的調用它, TestRunner.dll必須位於你的測試程序的路徑下。所以把/lib目錄下的testrunnerd.dll和 TestRunner.dll文件分別拷貝到UnitTest priject的程序debug和release版本輸出目錄中。 如下圖所示:
(這是release 版本)只要放在一起就可以了。
配置工作終於完成,下面開始寫測試框架。
在 CppUnit中, 是以TestCase為最小的測試單位, 若干TestCase組成一個TestSuite。所以我們 要先建立一個TestCase。
在UnitTest project中新建一個類, 命名為CPlusTestCase, 讓其從CppUnit::TestCase派生。為其新增一個方法,假設為 void testAdd(); 我們將在這個 函數中寫入我們的一些測試代碼(還記得我們要測試的構想中的CPlus::Add(…)嗎) 。代碼如下:切記要包含頭文件
#include <cppunit/TestCase.h>
class CPlusTestCase : public CppUnit::TestCase
{
public:
CPlusTestCase ();
virtual ~ CPlusTestCase ();
void testAdd ();
};
接下來, 我們要對我們的CPlusTestCase進行聲明。聲明用到了三個 宏.
CPPUNIT_TEST_SUITE();
CPPUNIT_TEST();
CPPUNIT_TEST_SUITE_END();
第一個宏聲明一個測試包(suite),第二個宏聲明(添 加)一個測試用例. 現在我們的CPlusTestCase類看上去象這樣:切記要包含頭文件,否則無 法識別這些宏。
#include <cppunit/TestCase.h>
通過這幾個宏,我們就把CPlusTestCase和testAdd注冊到了測試列表當 中。
#include <CppUnit/extensions/HelperMacros.h>
class CPlusTestCase : public CppUnit::TestCase
{
CPPUNIT_TEST_SUITE(CPlusTestCase);
CPPUNIT_TEST(testAdd);
CPPUNIT_TEST_SUITE_END();
public:
CPlusTestCase ();
virtual ~ CPlusTestCase ();
void testAdd ();
};
接下來,我們要注冊我們的測試suite. 使用 CPPUNIT_TEST_SUITE_NAMED_REGISTRATION()來注冊一個測試suite. 這個宏的第二個參數是 我們注冊的suite的名字. 在這裡我們可以用字符串來代替, 但我們用一個靜態函數來返回這 個suite的名字.
// PlusTestCase.h
class CPlusTestCase : public CppUnit::TestCase
{
CPPUNIT_TEST_SUITE(CPlusTestCase);
CPPUNIT_TEST(testAdd);
CPPUNIT_TEST_SUITE_END();
public:
CPlusTestCase ();
virtual ~ CPlusTestCase ();
void testAdd();
static std::string GetSuiteName();
};
// PlusTestCase.cpp
std::string CPlusTestCase::GetSuiteName()
{
return " CPlus ";
}
記得要在PlusTestCase.h中包含 #include <string>
然後在 PlusTestCase.cpp注冊我們的 suite.CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(CPlusTestCase, CPlusTestCase::GetSuiteName());
它將CPlusTestCase這個TestSuite注冊到一個 指定的TestFactory工廠中。
接下來我們寫一個注冊函數
static CppUnit::Test* GetSuite(), 使其在運行期生成一個Test.// PlusTestCase.h
class CPlusTestCase : public CppUnit::TestCase
{
CPPUNIT_TEST_SUITE (CPlusTestCase);
CPPUNIT_TEST(testAdd);
CPPUNIT_TEST_SUITE_END ();
public:
CPlusTestCase ();
virtual ~ CPlusTestCase ();
void testAdd();
static std::string GetSuiteName();
static CppUnit::Test* GetSuite();
};
// PlusTestCase.cpp
CppUnit::Test* CPlusTestCase::GetSuite()
{
CppUnit::TestFactoryRegistry& reg =
CppUnit::TestFactoryRegistry::getRegistry (CPlusTestCase::GetSuiteName());
return reg.makeTest();
}
記住在PlusTestCase.h中 包含頭文件:#include <cppunit/extensions/TestFactoryRegistry.h>
最後, 我們為單元測試建立一個UI測試界面.
由於我們希望這個Project運行後顯示的是 GUI界面,所以我們需要在App的 InitInstance ()中屏蔽掉原有的對話框,代之以CppUnit的 GUI。
我們在CUnitTestApp::InitInstance()函數中,將原先顯示主對話框的代碼以下 面的代碼取代:CppUnit::MfcUi::TestRunner runner;
runner.addTest (CPlusTestCase::GetSuite());//添加測試
runner.run();//show UI
/* CUnitTestDlg dlg;
m_pMainWnd = &dlg;
int nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
// TODO: Place code here to handle when the dialog is
// dismissed with OK
}
else if (nResponse == IDCANCEL)
{
// TODO: Place code here to handle when the dialog is
// dismissed with Cancel
}
*/
切記必須先在UnitTest.cpp中包含頭文 件:#include <cppunit/ui/mfc/TestRunner.h>
#include " PlusTestCase.h "
到此為止, 我們已經建立好一個簡單的單元測試框架。測試 框架雖然寫好了,但是測試代碼仍然為空,產品代碼也還沒有寫。下面我們來寫測試代碼:
如前所述,在測試類中,我們添加了一個測試方法:
void testAdd();
它測試的對象是前面提到的CPlus類的方法:int Add(int nNum1, int nNum2);(產品代碼) 我們來看看testAdd()的實現:記得在PlusTestCase.h中包含頭文件#include <cppunit/TestAssert.h>
CPPUNIT_ASSERT_EQUAL是一個判斷結果的宏。CppUnit中類 似的其它宏請查閱TestAssert.h,本文在此不做詳述 。
// PlusTestCase.cpp
void CPlusTestCase::testAdd()
{
CPlus plus;
int nResult = plus.Add(10, 20); //執行Add操作
CPPUNIT_ASSERT_EQUAL(30, nResult); //檢 查結果是否等於30
}
另外,我們還可以覆寫基類的 setUp()、tearDown()兩個函數。這兩個函數實際上是一個模板方法,在測試運行之前會調用 setUp()以進行一些初始化的工作,測試結束之後又會調用tearDown()來做一些“善後 工作” ,比如資源的回收等等。當然,你也可以不覆寫這兩個函數,因為它們在基類 裡定義成了空方法,而不是純虛函數。
編寫完上面的測試代碼後,進行編譯。編譯肯 定通不過,編譯器會告訴我們CPlus類沒有聲明,因為我們還沒有實現CPlus類呢!現在的工 作就是馬上實現CPlus類,讓編譯通過。現在你應該嗅到一點“測試驅動”(Test Driven Develop)的味道了吧?
在VC中建立一個MFC Extension Dll的Project,在這 個Project 中加入類CPlus,它的聲明如下:
// Plus.h
class AFX_EXT_CLASS CPlus
{
public:
CPlus();
virtual ~CPlus();
public:
int Add(int nNum1, int nNum2);
};
Add在Plus.cpp中實 現如下
int CPlus::Add(int nNum1, int nNum2)
{
return nNum1 + nNum2;//這裡可以寫一些錯誤的語句,用來看看錯誤的結果
}
非常簡 單,不是嗎?現在讓前面那個包含測試代碼的Project dependent這個Project,並且include 相關頭文件 ,Rebuild All,你會發現編譯已通過。你體會到了測試代碼驅動產品代碼了嗎? 當然我們的這個例子還很簡單 ,沒有重構這一步驟。
運行我們的測試程序,單擊Browse ,你就會看到如下圖所示的界面:
選擇 CPlusTestCase::testAdd後,單擊Run,你就會看到如下圖所示的界面:
這下你應該 對前面我們說的TestSuite的名字理解更深了吧。CPlus是一個測試包TestSuite,它的下面包 含一個測試用例,這個測試用例下面又包含一個測試方法。
如果我修改CPlus::Add的 代碼如下:
int CPlus::Add(int nNum1, int nNum2)
{
// return nNum1 + nNum2;
return 2;
}
重新編譯通過,運行程序就會發現:
GUI 顯示有一個單元測試不通過,並顯示出錯的地方和原因,這樣就很好的控制Bug了。
四、下面是完整的程序清單
// PlusTestCase.h
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
#include <string>
#include <cppunit/TestCase.h>
#include <CppUnit/extensions/HelperMacros.h>
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/TestAssert.h>
class CPlusTestCase : public CppUnit::TestCase
{
//通過這幾個宏,我們就把CPlusTestCase和testAdd注冊到了測試列表 當中.
CPPUNIT_TEST_SUITE(CPlusTestCase); //聲明一個測試包
CPPUNIT_TEST(testAdd); //聲明一個測試用例
CPPUNIT_TEST_SUITE_END();
public:
CPlusTestCase();
virtual ~CPlusTestCase();
void testAdd(); //測試方法
static std::string GetSuiteName();
//寫一個注冊函數, 使其在運行期生成一個Test
static CppUnit::Test* GetSuite();
};
// PlusTestCase.cpp
#include "stdafx.h"
#include "UnitTest.h"
#include "PlusTestCase.h"
#include "plus.h"
#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif
//注冊一個測試suite到一個指定的TestFactory工廠中
CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(CPlusTestCase, CPlusTestCase::GetSuiteName());
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
CPlusTestCase::CPlusTestCase()
{
}
CPlusTestCase::~CPlusTestCase()
{
}
void CPlusTestCase::testAdd ()
{
CPlus plus;
int nResult = plus.Add(10, 20); //執行Add 操作
CPPUNIT_ASSERT_EQUAL(30, nResult); //檢查結果是否等於30
}
std::string CPlusTestCase::GetSuiteName()
{
return "CPlus";
}
/*
* 注意:CPlusTestCase::GetSuite()返回一 個指向CppUnit::Test的指針.
* 這個指針就是整個測試的起點。
* CppUnit::TestFactoryRegistry::getRegistry()根據TestSuite的名字返回 TestFactoryRegistry工
* 然後調用工廠裡的makeTest()對TestSuite進行組裝,將建 立起一個樹狀的測試結構。
*/
CppUnit::Test* CPlusTestCase::GetSuite()
{
CppUnit::TestFactoryRegistry& reg = CppUnit::TestFactoryRegistry::getRegistry(CPlusTestCase::GetSuiteName());
return reg.makeTest();
}
// UnitTest.cpp
#include "stdafx.h"
#include "UnitTest.h"
#include <cppunit/ui/mfc/TestRunner.h>
#include "PlusTestCase.h"
…
/////////////////////////////////////////////////////////////////////////////// CUnitTestApp initialization
BOOL CUnitTestApp::InitInstance()
{
…
CppUnit::MfcUi::TestRunner runner;
runner.addTest(CPlusTestCase::GetSuite()); //添加測試 runner.addTest (CMinusTestCase::GetSuite());
runner.run(); //show UI
/* CUnitTestDlg dlg;
m_pMainWnd = &dlg;
int nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
// TODO: Place code here to handle when the dialog is
// dismissed with OK
}
else if (nResponse == IDCANCEL)
{
// TODO: Place code here to handle when the dialog is
// dismissed with Cancel
}
*/
return FALSE;
}
五、參考資料
Cpluser《CppUnit測試框架入門》
Freefalcon《CppUnit快速入門》
《使 用CppUnit進行單元測試》
感謝:Cpluser 和 Freefalcon 對 CppUnit 的介紹。
下載源代碼:http://www.vckbase.com/code/downcode.asp?id=3032