- 论坛徽章:
- 0
|
[文章]CppUnit - 测试驱动开发入门
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 <string>;
#include <iostream>;
#include <exception>;
#include <windows.h>;
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<MyTestCase>; testCase1( "testCtorAndGetName", MyTestCase::testCtorAndGetName );
CPPUNIT_NS::TestCaller<MyTestCase>; testCase2( "testGetFileSize", MyTestCase::testGetFileSize );
CPPUNIT_NS::TestCaller<MyTestCase>; testCase3( "testFileExist", MyTestCase::testFileExist );
CPPUNIT_NS::TestCaller<MyTestCase>; testCase4( "testFileModifyDateBasic", MyTestCase::testFileModifyDateBasic );
CPPUNIT_NS::TestCaller<MyTestCase>; 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 <string>;
#include <iostream>;
#include <exception>;
#include <windows.h>;
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<MyTestCase>; testCase1( "testCtorAndGetName", MyTestCase::testCtorAndGetName );
CPPUNIT_NS::TestCaller<MyTestCase>; testCase2( "testGetFileSize", MyTestCase::testGetFileSize );
CPPUNIT_NS::TestCaller<MyTestCase>; testCase3( "testFileExist", MyTestCase::testFileExist );
CPPUNIT_NS::TestCaller<MyTestCase>; testCase4( "testFileModifyDateBasic", MyTestCase::testFileModifyDateBasic );
CPPUNIT_NS::TestCaller<MyTestCase>; 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!
其实上面的测试以及实现代码还有一些问题,譬如说,测试用例分得还不够细,有些测试可以继续细分为几个函数,这样一旦遇到测试错误,你可以很精确的知道错误的位置(因为抛出异常错误是不能知道行数的)。不过用来说明怎样进行测试驱动开发应该是足够了。 |
|