UML软件工程组织

测试驱动开发入门-CppUnit
作者:snaill

测试驱动开发是一个现在软件界最流行的词汇之一,可是很多人还是不得其门而入。这篇文章想通过对于 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 testCase1( "testCtorAndGetName", MyTestCase::testCtorAndGetName );
CPPUNIT_NS::TestCaller testCase2( "testGetFileSize", MyTestCase::testGetFileSize );
CPPUNIT_NS::TestCaller testCase3( "testFileExist", MyTestCase::testFileExist );
CPPUNIT_NS::TestCaller testCase4( "testFileModifyDateBasic", MyTestCase::testFileModifyDateBasic );
CPPUNIT_NS::TestCaller testCase5( "testFileModifyDateEqual", MyTestCase::testFileModifyDateEqual );
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 testCase1( "testCtorAndGetName", MyTestCase::testCtorAndGetName );
CPPUNIT_NS::TestCaller testCase2( "testGetFileSize", MyTestCase::testGetFileSize );
CPPUNIT_NS::TestCaller testCase3( "testFileExist", MyTestCase::testFileExist );
CPPUNIT_NS::TestCaller testCase4( "testFileModifyDateBasic", MyTestCase::testFileModifyDateBasic );
CPPUNIT_NS::TestCaller testCase5( "testFileModifyDateEqual", MyTestCase::testFileModifyDateEqual );
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 testCase1( "testCtorAndGetName", MyTestCase::testCtorAndGetName );
CPPUNIT_NS::TestCaller testCase2( "testGetFileSize", MyTestCase::testGetFileSize );
CPPUNIT_NS::TestCaller testCase3( "testFileExist", MyTestCase::testFileExist );
CPPUNIT_NS::TestCaller testCase4( "testFileModifyDateBasic", MyTestCase::testFileModifyDateBasic );
CPPUNIT_NS::TestCaller testCase5( "testFileModifyDateEqual", MyTestCase::testFileModifyDateEqual );

这段代码虽然还不够触目惊心,但是让程序员来做这个,的确是太浪费了。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中对于这两个函数的介绍。

 

版权所有:UML软件工程组织