測試驅動開發是一個現在軟件界流行的詞匯之一,可是很多人還是不得其門而入。這篇文章想通過對于CppUnit的介紹,給予讀者一個基本的映像。如果你熟知CppUnit的使用,請參閱我的另一篇文章:CppUnit代碼簡介 - 第一部分,核心類來獲得對于CppUnit進一步的了解。
II. 測試驅動開發
要理解測試驅動開發,必須先理解測試。測試是通過對源代碼的運行或者別的方式的檢測來確定源代碼之中是否含有已知或者未知的錯誤。所謂測試驅動開發,是在開發前根據對將要開發的程序的要求,先寫好所有測試代碼,并且在開發過程中不時地通過運行測試代碼來獲得所開發的代碼與所要求的結果之間的差距。很多人可能會有疑問:既然我還沒有開始寫代碼,我怎么能夠寫測試代碼呢?這是因為,雖然我們還沒有寫出任何實現代碼,但是我們可以根據我們對代碼的要求從使用者的角度寫出測試代碼。事實上,在開發前寫出測試代碼,可以檢測你的要求是不是完善和精確,因為如果你寫不出測試代碼,表示你的需求還不夠清晰。
這篇文章通過一個文件狀態操作類來展示測試驅動開發相對于普通開發方法的優勢。
III. 文件狀態操作類(FileStatus)需求
構造函數,接受一個const std::string&作為文件名參數。
DWORD getFileSize()函數,獲取這個文件的長度。
bool fileExists()函數,獲取這個文件是否存在。
void setFileModifyDate(FILETIME ft)函數,設定這個文件的修改日期。
FILETIME getFileModifyDate()函數,返回這個文件的修改日期。
std::string getFileName()函數,返回這個文件的名字。
IV. CppUnit簡介
我們所進行的測試,某種意義上說,是一個或者多個函數。通過對這些函數的運行,我們可以檢測我們是否有錯誤。假設我們要對構造函數和getFileName函數進行測試,這里面有一個很顯然的不變式,是對一個FileStatus::getFileName函數的調用,應該與傳給這個FileStatus對象的構造函數的參數相同。于是我們有這樣一個函數:
bool testCtorAndGetFileName()
{
const string fileName( "a.dat" );
FileStatus status( fileName );
return ( status.getFileName() == fileName );
}
我們只需要測試這個函數的返回值可以知道是否正確了。在CppUnit中,我們可以從TestCase派生出一個類,并且重載它的runTest函數。
class MyTestCase:public CPPUNIT_NS::TestCase
{
public:
virtual void runTest()
{
const std::string fileName( "a.dat" );
FileStatus status( fileName );
CPPUNIT_ASSERT_EQUAL( status.getFileName(), fileName );
}
};
CPPUNIT_ASSERT_EQUAL是一個宏,在它的兩個參數不相等的時候,會拋出異常。所以,理論上說,我們可以通過:
MyTestCase m;
m.runTest();
來進行測試,如果有異常拋出,那么說明代碼寫錯了。可是,這顯然不方便,也不是我們使用CppUnit的初衷。下面我們給出完整的代碼:
// UnitTest.cpp : Defines the entry point for the console application.
//
#include "CppUnit/TestCase.h"
#include "CppUnit/TestResult.h"
#include "CppUnit/TextOutputter.h"
#include "CppUnit/TestResultCollector.h"
#include
#include
class FileStatus
{
std::string mFileName;
public:
FileStatus( const std::string& fileName ):mFileName( fileName )
{}
std::string getFileName() const
{
return mFileName;
}
};
class MyTestCase:public CPPUNIT_NS::TestCase
{
public:
virtual void runTest()
{
const std::string fileName( "a.dat" );
FileStatus status( fileName );
CPPUNIT_ASSERT_EQUAL( status.getFileName(), fileName );
}
};
int main()
{
MyTestCase m;
CPPUNIT_NS::TestResult r;
CPPUNIT_NS::TestResultCollector result;
r.addListener( &result );
m.run( &r );
CPPUNIT_NS::TextOutputter out( &result, std::cout );
out.write();
return 0;
}
這里我先說一下怎樣運行這個程序。假設你的CppUnit版本是1.10.2,解壓后,你會在src文件夾中,發現一個CppUnitLibraries.dsw,打開它,并且編譯。你會在lib文件夾中,發現一些 lib和dll,我們的程序需要依賴當中的某些。接著,創建一個Console應用程序,假設我們僅使用Debug模式,在Project Settings中,把預編譯選項(Precompiled Header)選成No,把CppUnit的include路徑加入到Additional Include Directories中,并且把Code Generation改成Multi-threaded Debug Dll,接著把CppUnitD.lib加入到你的項目中去。后把我們的這個文件替換main.cpp。這個時候,可以編譯運行了。
這個文件中,前面四行分別是CppUnit相應的頭文件,在CppUnit中,通常某個類定義在用它的類名命名的頭文件中。接著是我們的string和 iostream頭文件。然后是我們類的一個簡單實現,只實現了這個測試中有意義的功能。接下去是我們的TestCase的定義,CPPUNIT_NS是 CppUnit所在的名字空間。main中,TestResult其實是一個測試的控制器,你在調用TestCase的run時,需要提供一個 TestResult。run作為測試的進行方,會把測試中產生的信息發送給TestResult,而TestResult作為一個分發器,會把所收到的信息再轉發給它的Listener。也是說,我簡單的定義一個TestResult并且把它的指針傳給TestCase::run,這個程序也能夠編譯通過并且正確運行,但是它不會有任何輸出。TestResultCollector可以把測試輸出的信息都收集起來,并且后通過 TextOutputter輸出出來。在上述的例子中,你所獲得的輸出是:
OK (1 tests)
這說明我們一共進行了1個測試,并且都通過了。如果我們人為地把"return mFileName;"改成"return mFileName + 'a';"以制造一個錯誤,那么測試的結果會變成:
!!!FAILURES!!!
Test Results:
Run: 1 Failures: 1 Errors: 0
1) test: (F) line: 31 c:unittestunittest.cpp
equality assertion failed
- Expected: a.data
- Actual : a.dat