javascript:;" onClick="javascript:tagshow(event, '%B2%E2%CA%D4');" target="_self">測試驅動開發是一個現在軟件界最流行的詞匯之一,可是很多人還是不得其門而入。這篇文章想通過對于 CppUnit的介紹,給予讀者一個基本的映像。
如果你熟知CppUnit的使用,請參閱我的另一篇文章:
CppUnit代碼簡介 - 第一部分,核心類來獲得對于CppUnit進一步的了解。
如果你熟知CppUnit的使用,請參閱我的另一篇文章:
I. 前言
測試驅動開發是一個現在軟件界最流行的詞匯之一,可是很多人還是不得其門而入。這篇文章想通過對于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
這個結果告訴我們我們的實現出現了問題。前面說到, CPPUNIT_ASSERT_EQUAL在兩個參數不等時會拋出異常,可是這里為什么沒有異常退出了?這是因為,我們執行每一個TestCase的 run的時候,它使用了一種特殊的機制把函數包起來,任何異常都會被捕獲。具體細節請參考我的CppUnit代碼簡介一文。
如果我們把#include "CppUnit/TextOutputter.h"替換成#include "CppUnit/CompilerOutputter.h",并且把TextOutputter替換成CompilerOutputter,輸出就變成:
c:unittestunittest.cpp(32) : error : Assertion
Test name:
equality assertion failed
- Expected: a.data
- Actual : a.dat
Failures !!!
Run: 1 Failure total: 1 Failures: 1 Errors: 0
這個輸出,在編譯器的信息窗口里面,可以通過雙擊文件名加行號的那一行來到達相應的位置。
V. 迭代開發
上面的例子中我們先針對需求的一部分寫了測試用例,然后就實現了相應的功能。我們可以在這些功能被測試后,繼續實現別的功能的測試用例,然后繼續實現相應的功能,這是一個迭代的過程,我們不斷地增加測試用例和實現代碼,最后達成需求。還有一種方法是,先寫好所有的測試用例(這個時候通常會編譯不通過),然后再添加能夠讓編譯通過所需要的實現(這個時候通常運行測試會有很多錯誤),接著通過正確實現使得沒有任何測試錯誤,最后,對代碼作優化和更新,并且不斷的保證測試通過。在這里我們著重介紹第二種方法。首先我們先寫下所有的測試用例,在這里,由于有很多測試用例,我們不再使用TestCase,因為TestCase通常用在單一測試任務的情況下。這次我們從 TestFixture派生我們的測試類:
class MyTestCase:public CPPUNIT_NS::TestFixture
{
public:
void testCtorAndGetName()
{
const std::string fileName( "a.dat" );
FileStatus status( fileName );
CPPUNIT_ASSERT_EQUAL( status.getFileName(), fileName );
}
void testGetFileSize()
{
const std::string fileName( "a.dat" );
FileStatus status( fileName );
CPPUNIT_ASSERT_EQUAL( status.getFileSize(), 0 );//?
}
};
寫到這里,我們發現了兩個問題,首先我們不停的初始化一些測試所需的對象,重復了很多代碼;其次我們發現了一個接口設計錯誤,我們的接口設計上沒有考慮一個文件不存在的情況。從中可見,先寫好測試用例,不僅是對實現的測試,也是對我們設計的測試。TestFixture定義了兩個成員函數setUp和tearDown,在每一個測試用例被執行的時候,和它定義在同一個類內部的setUp和tearDown會被調用以進行初始化和清除工作。我們可以用這兩個函數來進行統一的初始化代碼。并且,我們修改 getFileSize、setFileModifyDate和getFileModifyDate使得它們在出現錯誤的時候,拋出異常 FileStatusError。下面是我們的測試用例:
class MyTestCase:public CPPUNIT_NS::TestFixture
{
std::string mFileNameExist;
std::string mFileNameNotExist;
std::string mTestFolder;
enum DUMMY
{
FILE_SIZE = 1011
};
public:
virtual void setUp()
{
mTestFolder = "c:justfortest";
mFileNameExist = mTestFolder + "exist.dat";
mFileNameNotExist = mTestFolder + "notexist.dat";
if( GetFileAttributes( mTestFolder.c_str() ) != INVALID_FILE_ATTRIBUTES )
throw std::exception( "test folder already exists" );
if( ! CreateDirectory( mTestFolder.c_str() ,NULL ) )
throw std::exception( "cannot create folder" );
HANDLE file = CreateFile( mFileNameExist.c_str(), GENERIC_READ | GENERIC_WRITE,
0, NULL, CREATE_NEW, 0, NULL );
if( file == INVALID_HANDLE_VALUE )
throw std::exception( "cannot create file" );
char buffer[FILE_SIZE];
DWORD bytesWritten;
if( !WriteFile( file, buffer, FILE_SIZE, &bytesWritten, NULL ) ||
bytesWritten != FILE_SIZE )
{
CloseHandle( file );
throw std::exception( "cannot write file" );
}
CloseHandle( file );
}
virtual void tearDown()
{
if( ! DeleteFile( mFileNameExist.c_str() ) )
throw std::exception( "cannot delete file" );
if( ! RemoveDirectory( mTestFolder.c_str() ) )
throw std::exception( "cannot remove folder" );
}
void testCtorAndGetName()
{
FileStatus status( mFileNameExist );
CPPUNIT_ASSERT_EQUAL( status.getFileName(), mFileNameExist );
}
void testGetFileSize()
{
FileStatus exist( mFileNameExist );
//這里FILE_SIZE缺省是int,而getFileSize返回DWORD,不加轉換會導致模版不能正確匹配。
CPPUNIT_ASSERT_EQUAL( exist.getFileSize(), (DWORD)FILE_SIZE );
FileStatus notExist( mFileNameNotExist );
CPPUNIT_ASSERT_THROW( notExist.getFileSize(), FileStatusError );
}
void testFileExist()
{
FileStatus exist( mFileNameExist );
CPPUNIT_ASSERT( exist.fileExist() );
FileStatus notExist( mFileNameNotExist );
CPPUNIT_ASSERT( ! notExist.fileExist() );
}
void testFileModifyDateBasic()
{
FILETIME fileTime;
GetSystemTimeAsFileTime( &fileTime );
FileStatus exist( mFileNameExist );
CPPUNIT_ASSERT_NO_THROW( exist.getFileModifyDate() );
CPPUNIT_ASSERT_NO_THROW( exist.setFileModifyDate( &fileTime ) );
FileStatus notExist( mFileNameNotExist );
CPPUNIT_ASSERT_THROW( notExist.getFileModifyDate(), FileStatusError );
CPPUNIT_ASSERT_THROW( notExist.setFileModifyDate( &fileTime ), FileStatusError );
}
void testFileModifyDateEqual()
{
FILETIME fileTime;
GetSystemTimeAsFileTime( &fileTime );
FileStatus exist( mFileNameExist );
CPPUNIT_ASSERT_NO_THROW( exist.setFileModifyDate( &fileTime ) );
FILETIME get = exist.getFileModifyDate();
// 這里 FILETIME 沒有定義 operator==,所以不能直接使用 CPPUNIT_ASSERT_EQUAL
CPPUNIT_ASSERT( CompareFileTime( &get, &fileTime ) == 0 );
}
};
接著我們編寫一個FileStatus類的骨架,使得這段測試代碼可以被編譯通過。
class FileStatusError
{};
class FileStatus
{
public:
FileStatus(const std::string& fileName)
{}
DWORD getFileSize() const
{
return 0;
}
bool fileExist() const
{
return false;
}
void setFileModifyDate( const FILETIME* )
{
}
FILETIME getFileModifyDate() const
{
return FILETIME();
}
std::string getFileName() const
{
return "";
}
};
下面給出完整的程序:
// 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 "CppUnit/TestCaller.h"
#include "CppUnit/extensions/HelperMacros.h"
#include
#include
#include
#include
class FileStatusError
{};
class FileStatus
{
public:
FileStatus(const std::string& fileName)
{}
DWORD getFileSize() const
{
return 0;
}
bool fileExist() const
{
return false;
}
void setFileModifyDate( const FILETIME* )
{
}
FILETIME getFileModifyDate() const
{
return FILETIME();
}
std::string getFileName() const
{
return "";
}
};
class MyTestCase:public CPPUNIT_NS::TestFixture
{
std::string mFileNameExist;
std::string mFileNameNotExist;
std::string mTestFolder;
enum DUMMY
{
FILE_SIZE = 1011
};
public:
virtual void setUp()
{
mTestFolder = "c:justfortest";
mFileNameExist = mTestFolder + "exist.dat";
mFileNameNotExist = mTestFolder + "notexist.dat";
if( GetFileAttributes( mTestFolder.c_str() ) != INVALID_FILE_ATTRIBUTES )
throw std::exception( "test folder already exists" );
if( ! CreateDirectory( mTestFolder.c_str() ,NULL ) )
throw std::exception( "cannot create folder" );
HANDLE file = CreateFile( mFileNameExist.c_str(), GENERIC_READ | GENERIC_WRITE,
0, NULL, CREATE_NEW, 0, NULL );
if( file == INVALID_HANDLE_VALUE )
throw std::exception( "cannot create file" );
char buffer[FILE_SIZE];
DWORD bytesWritten;
if( !WriteFile( file, buffer, FILE_SIZE, &bytesWritten, NULL ) ||
bytesWritten != FILE_SIZE )
{
CloseHandle( file );
throw std::exception( "cannot write file" );
}
CloseHandle( file );
}
virtual void tearDown()
{
if( ! DeleteFile( mFileNameExist.c_str() ) )
throw std::exception( "cannot delete file" );
if( ! RemoveDirectory( mTestFolder.c_str() ) )
throw std::exception( "cannot remove folder" );
}
void testCtorAndGetName()
{
FileStatus status( mFileNameExist );
CPPUNIT_ASSERT_EQUAL( status.getFileName(), mFileNameExist );
}
void testGetFileSize()
{
FileStatus exist( mFileNameExist );
CPPUNIT_ASSERT_EQUAL( exist.getFileSize(), (DWORD)FILE_SIZE );
FileStatus notExist( mFileNameNotExist );
CPPUNIT_ASSERT_THROW( notExist.getFileSize(), FileStatusError );
}
void testFileExist()
{
FileStatus exist( mFileNameExist );
CPPUNIT_ASSERT( exist.fileExist() );
FileStatus notExist( mFileNameNotExist );
CPPUNIT_ASSERT( ! notExist.fileExist() );
}
void testFileModifyDateBasic()
{
FILETIME fileTime;
GetSystemTimeAsFileTime( &fileTime );
FileStatus exist( mFileNameExist );
CPPUNIT_ASSERT_NO_THROW( exist.getFileModifyDate() );
CPPUNIT_ASSERT_NO_THROW( exist.setFileModifyDate( &fileTime ) );
FileStatus notExist( mFileNameNotExist );
CPPUNIT_ASSERT_THROW( notExist.getFileModifyDate(), FileStatusError );
CPPUNIT_ASSERT_THROW( notExist.setFileModifyDate( &fileTime ), FileStatusError );
}
void testFileModifyDateEqual()
{
FILETIME fileTime;
GetSystemTimeAsFileTime( &fileTime );
FileStatus exist( mFileNameExist );
CPPUNIT_ASSERT_NO_THROW( exist.setFileModifyDate( &fileTime ) );
FILETIME get = exist.getFileModifyDate();
CPPUNIT_ASSERT( CompareFileTime( &get, &fileTime ) == 0 );
}
};
int main()
{
CPPUNIT_NS::TestResult r;
CPPUNIT_NS::TestResultCollector result;
r.addListener( &result );
CPPUNIT_NS::TestCaller
CPPUNIT_NS::TestCaller
CPPUNIT_NS::TestCaller
CPPUNIT_NS::TestCaller
CPPUNIT_NS::TestCaller
testCase1.run( &r );
testCase2.run( &r );
testCase3.run( &r );
testCase4.run( &r );
testCase5.run( &r );
CPPUNIT_NS::TextOutputter out( &result, std::cout );
out.write();
return 0;
}
這里的TestCaller可以把從TestFixture派生而來的類的成員函數轉化為一個TestCase。這段代碼可以編譯通過,運行后一共進行了5個測試,完全失敗。這是我們意料之中的結果,因此我們進一步實現我們的功能,完成后的代碼為:
// 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 "CppUnit/TestCaller.h"
#include "CppUnit/extensions/HelperMacros.h"
#include
#include
#include
#include
class FileStatusError
{};
class FileStatus
{
std::string mFileName;
public:
FileStatus(const std::string& fileName):mFileName( fileName )
{}
DWORD getFileSize() const
{
DWORD fileSize = INVALID_FILE_SIZE;
HANDLE file = CreateFile( mFileName.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING, 0, NULL );
if( file != INVALID_HANDLE_VALUE )
{
fileSize = GetFileSize( file, NULL );
CloseHandle( file );
}
if( fileSize == INVALID_FILE_SIZE )
throw FileStatusError();
return fileSize;
}
bool fileExist() const
{
return GetFileAttributes( mFileName.c_str() ) != INVALID_FILE_ATTRIBUTES;
}
void setFileModifyDate( const FILETIME* fileTime )
{
BOOL result = FALSE;
HANDLE file = CreateFile( mFileName.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING, 0, NULL );
if( file != INVALID_HANDLE_VALUE )
{
result = SetFileTime( file, NULL, NULL, fileTime );
int i = GetLastError();
CloseHandle( file );
}
if( ! result )
throw FileStatusError();
}
FILETIME getFileModifyDate() const
{
FILETIME time;
BOOL result = FALSE;
HANDLE file = CreateFile( mFileName.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING, 0, NULL );
if( file != INVALID_HANDLE_VALUE )
{
result = GetFileTime( file, NULL, NULL, &time );
CloseHandle( file );
}
if( ! result )
throw FileStatusError();
return time;
}
std::string getFileName() const
{
return mFileName;
}
};
class MyTestCase:public CPPUNIT_NS::TestFixture
{
std::string mFileNameExist;
std::string mFileNameNotExist;
std::string mTestFolder;
enum DUMMY
{
FILE_SIZE = 1011
};
public:
virtual void setUp()
{
mTestFolder = "c:justfortest";
mFileNameExist = mTestFolder + "exist.dat";
mFileNameNotExist = mTestFolder + "notexist.dat";
if( GetFileAttributes( mTestFolder.c_str() ) != INVALID_FILE_ATTRIBUTES )
throw std::exception( "test folder already exists" );
if( ! CreateDirectory( mTestFolder.c_str() ,NULL ) )
throw std::exception( "cannot create folder" );
HANDLE file = CreateFile( mFileNameExist.c_str(), GENERIC_READ | GENERIC_WRITE,
0, NULL, CREATE_NEW, 0, NULL );
if( file == INVALID_HANDLE_VALUE )
throw std::exception( "cannot create file" );
char buffer[FILE_SIZE];
DWORD bytesWritten;
if( !WriteFile( file, buffer, FILE_SIZE, &bytesWritten, NULL ) ||
bytesWritten != FILE_SIZE )
{
CloseHandle( file );
throw std::exception( "cannot write file" );
}
CloseHandle( file );
}
virtual void tearDown()
{
if( ! DeleteFile( mFileNameExist.c_str() ) )
throw std::exception( "cannot delete file" );
if( ! RemoveDirectory( mTestFolder.c_str() ) )
throw std::exception( "cannot remove folder" );
}
void testCtorAndGetName()
{
FileStatus status( mFileNameExist );
CPPUNIT_ASSERT_EQUAL( status.getFileName(), mFileNameExist );
}
void testGetFileSize()
{
FileStatus exist( mFileNameExist );
CPPUNIT_ASSERT_EQUAL( exist.getFileSize(), (DWORD)FILE_SIZE );
FileStatus notExist( mFileNameNotExist );
CPPUNIT_ASSERT_THROW( notExist.getFileSize(), FileStatusError );
}
void testFileExist()
{
FileStatus exist( mFileNameExist );
CPPUNIT_ASSERT( exist.fileExist() );
FileStatus notExist( mFileNameNotExist );
CPPUNIT_ASSERT( ! notExist.fileExist() );
}
void testFileModifyDateBasic()
{
FILETIME fileTime;
GetSystemTimeAsFileTime( &fileTime );
FileStatus exist( mFileNameExist );
CPPUNIT_ASSERT_NO_THROW( exist.getFileModifyDate() );
CPPUNIT_ASSERT_NO_THROW( exist.setFileModifyDate( &fileTime ) );
FileStatus notExist( mFileNameNotExist );
CPPUNIT_ASSERT_THROW( exist.getFileModifyDate(), FileStatusError );
CPPUNIT_ASSERT_THROW( exist.setFileModifyDate( &fileTime ), FileStatusError );
}
void testFileModifyDateEqual()
{
FILETIME fileTime;
GetSystemTimeAsFileTime( &fileTime );
FileStatus exist( mFileNameExist );
CPPUNIT_ASSERT_NO_THROW( exist.setFileModifyDate( &fileTime ) );
FILETIME get = exist.getFileModifyDate();
CPPUNIT_ASSERT( CompareFileTime( &get, &fileTime ) == 0 );
}
};
int main()
{
CPPUNIT_NS::TestResult r;
CPPUNIT_NS::TestResultCollector result;
r.addListener( &result );
CPPUNIT_NS::TestCaller
CPPUNIT_NS::TestCaller
CPPUNIT_NS::TestCaller
CPPUNIT_NS::TestCaller
CPPUNIT_NS::TestCaller
testCase1.run( &r );
testCase2.run( &r );
testCase3.run( &r );
testCase4.run( &r );
testCase5.run( &r );
CPPUNIT_NS::TextOutputter out( &result, std::cout );
out.write();
return 0;
}
運行測試,發現兩個錯誤:
1) test: testFileModifyDateBasic (F) line: 140 c:unittestunittest.cpp
assertion failed
- Unexpected exception caught
2) test: testFileModifyDateEqual (F) line: 150 c:unittestunittest.cpp
assertion failed
- Unexpected exception caught
調試發現,原來我的setFileModifyDate中,文件的打開方式為GENERIC_READ,只有讀權限,自然不能寫。把這個替換為 GENERIC_READ | GENERIC_WRITE,再運行,一切OK!
其實上面的測試以及實現代碼還有一些問題,譬如說,測試用例分得還不夠細,有些測試可以繼續細分為幾個函數,這樣一旦遇到測試錯誤,你可以很精確的知道錯誤的位置(因為拋出異常錯誤是不能知道行數的)。不過用來說明怎樣進行測試驅動開發應該是足夠了。
VI. 測試集
CPPUNIT_NS::TestCaller
CPPUNIT_NS::TestCaller
CPPUNIT_NS::TestCaller
CPPUNIT_NS::TestCaller
CPPUNIT_NS::TestCaller
這段代碼雖然還不夠觸目驚心,但是讓程序員來做這個,的確是太浪費了。CppUnit為我們提供了一些機制來避免這樣的浪費。我們可以修改我們的測試代碼為:
class MyTestCase:public CPPUNIT_NS::TestFixture
{
std::string mFileNameExist;
std::string mFileNameNotExist;
std::string mTestFolder;
enum DUMMY
{
FILE_SIZE = 1011
};
CPPUNIT_TEST_SUITE( MyTestCase );
CPPUNIT_TEST( testCtorAndGetName );
CPPUNIT_TEST( testGetFileSize );
CPPUNIT_TEST( testFileExist );
CPPUNIT_TEST( testFileModifyDateBasic );
CPPUNIT_TEST( testFileModifyDateEqual );
CPPUNIT_TEST_SUITE_END();
public:
virtual void setUp()
{
mTestFolder = "c:justfortest";
mFileNameExist = mTestFolder + "exist.dat";
mFileNameNotExist = mTestFolder + "notexist.dat";
if( GetFileAttributes( mTestFolder.c_str() ) != INVALID_FILE_ATTRIBUTES )
throw std::exception( "test folder already exists" );
if( ! CreateDirectory( mTestFolder.c_str() ,NULL ) )
throw std::exception( "cannot create folder" );
HANDLE file = CreateFile( mFileNameExist.c_str(), GENERIC_READ | GENERIC_WRITE,
0, NULL, CREATE_NEW, 0, NULL );
if( file == INVALID_HANDLE_VALUE )
throw std::exception( "cannot create file" );
char buffer[FILE_SIZE];
DWORD bytesWritten;
if( !WriteFile( file, buffer, FILE_SIZE, &bytesWritten, NULL ) ||
bytesWritten != FILE_SIZE )
{
CloseHandle( file );
throw std::exception( "cannot write file" );
}
CloseHandle( file );
}
virtual void tearDown()
{
if( ! DeleteFile( mFileNameExist.c_str() ) )
throw std::exception( "cannot delete file" );
if( ! RemoveDirectory( mTestFolder.c_str() ) )
throw std::exception( "cannot remove folder" );
}
void testCtorAndGetName()
{
FileStatus status( mFileNameExist );
CPPUNIT_ASSERT_EQUAL( status.getFileName(), mFileNameExist );
}
void testGetFileSize()
{
FileStatus exist( mFileNameExist );
CPPUNIT_ASSERT_EQUAL( exist.getFileSize(), (DWORD)FILE_SIZE );
FileStatus notExist( mFileNameNotExist );
CPPUNIT_ASSERT_THROW( notExist.getFileSize(), FileStatusError );
}
void testFileExist()
{
FileStatus exist( mFileNameExist );
CPPUNIT_ASSERT( exist.fileExist() );
FileStatus notExist( mFileNameNotExist );
CPPUNIT_ASSERT( ! notExist.fileExist() );
}
void testFileModifyDateBasic()
{
FILETIME fileTime;
GetSystemTimeAsFileTime( &fileTime );
FileStatus exist( mFileNameExist );
CPPUNIT_ASSERT_NO_THROW( exist.getFileModifyDate() );
CPPUNIT_ASSERT_NO_THROW( exist.setFileModifyDate( &fileTime ) );
FileStatus notExist( mFileNameNotExist );
CPPUNIT_ASSERT_THROW( notExist.getFileModifyDate(), FileStatusError );
CPPUNIT_ASSERT_THROW( notExist.setFileModifyDate( &fileTime ), FileStatusError );
}
void testFileModifyDateEqual()
{
FILETIME fileTime;
GetSystemTimeAsFileTime( &fileTime );
FileStatus exist( mFileNameExist );
CPPUNIT_ASSERT_NO_THROW( exist.setFileModifyDate( &fileTime ) );
FILETIME get = exist.getFileModifyDate();
CPPUNIT_ASSERT( CompareFileTime( &get, &fileTime ) == 0 );
}
};
CPPUNIT_TEST_SUITE_REGISTRATION( MyTestCase );
int main()
{
CPPUNIT_NS::TestResult r;
CPPUNIT_NS::TestResultCollector result;
r.addListener( &result );
CPPUNIT_NS::TestFactoryRegistry::getRegistry().makeTest()->run( &r );
CPPUNIT_NS::TextOutputter out( &result, std::cout );
out.write();
return 0;
}
這里的
CPPUNIT_TEST_SUITE( MyTestCase );
CPPUNIT_TEST( testCtorAndGetName );
CPPUNIT_TEST( testGetFileSize );
CPPUNIT_TEST( testFileExist );
CPPUNIT_TEST( testFileModifyDateBasic );
CPPUNIT_TEST( testFileModifyDateEqual );
CPPUNIT_TEST_SUITE_END();
最重要的內容其實是定義了一個函數suite,這個函數返回了一個包含了所有CPPUNIT_TEST定義的測試用例的一個測試集。CPPUNIT_TEST_SUITE_REGISTRATION通過靜態注冊把這個測試集注冊到全局的測試樹中,最后通過CPPUNIT_NS::TestFactoryRegistry::getRegistry(). makeTest()生成一個包含所有測試用例的測試并且運行。具體的內部運行機制請參考CppUnit代碼簡介。
VII. 小節
這篇文章簡要的介紹了CppUnit和測試驅動開發的基本概念,雖然CppUnit還有很多別的功能,譬如說基于GUI的測試環境以及和編譯器Post Build相連接的測試輸出,以及對于測試系統的擴展等,但是基本上掌握了本文中的內容就可以進行測試驅動的開發了。
此外,測試驅動開發還可以檢驗需求的錯誤。其實我選用GetFileTime和 SetFileTime作為例子是因為,有些系統上,SetFileTime所設置的時間是有一定的精度的,譬如說按秒,按天,...,因此你設置了一個時間后,可能get回來的時間和它不同。這其實是一個需求的錯誤。當然由于我的系統上沒有這個問題,所以我也就不無病呻吟了。具體可以參考MSDN中對于這兩個函數的介紹。