I\'m still having issues justifying TDD to myself. As I have mentioned in other questions, 90% of the code I write does absolutely nothing but
See below for FindFirstFile/FindNextFile/FindClose example
I use googlemock. For an external API I generally create an interface class. Assume I was going to call fopen, fwrite, fclose
class FileIOInterface {
public:
~virtual FileIOInterface() {}
virtual FILE* Open(const char* filename, const char* mode) = 0;
virtual size_t Write(const void* data, size_t size, size_t num, FILE* file) = 0;
virtual int Close(FILE* file) = 0;
};
The actual implementation would be this
class FileIO : public FileIOInterface {
public:
virtual FILE* Open(const char* filename, const char* mode) {
return fopen(filename, mode);
}
virtual size_t Write(const void* data, size_t size, size_t num, FILE* file) {
return fwrite(data, size, num, file);
}
virtual int Close(FILE* file) {
return fclose(file);
}
};
Then using googlemock I make a MockFileIO class like this
class MockFileIO : public FileIOInterface {
public:
virtual ~MockFileIO() { }
MOCK_MEHTOD2(Open, FILE*(const char* filename, const char* mode));
MOCK_METHOD4(Write, size_t(const void* data, size_t size, size_t num, FILE* file));
MOCK_METHOD1(Close, int(FILE* file));
}
This makes writing the tests easy. I don't have to provide a test implementation of Open/Write/Close. googlemock handles that for me. as in. (note I use googletest for my unit testing framework.)
Assume I have a function like this that needs testing
// Writes a file, returns true on success.
bool WriteFile(FileIOInterface fio, const char* filename, const void* data, size_size) {
FILE* file = fio.Open(filename, "wb");
if (!file) {
return false;
}
if (fio.Write(data, 1, size, file) != size) {
return false;
}
if (fio.Close(file) != 0) {
return false;
}
return true;
}
And here's the tests.
TEST(WriteFileTest, SuccessWorks) {
MockFileIO fio;
static char data[] = "hello";
const char* kName = "test";
File test_file;
// Tell the mock to expect certain calls and what to
// return on those calls.
EXPECT_CALL(fio, Open(kName, "wb")
.WillOnce(Return(&test_file));
EXPECT_CALL(fio, Write(&data, 1, sizeof(data), &test_file))
.WillOnce(Return(sizeof(data)));
EXPECT_CALL(file, Close(&test_file))
.WillOnce(Return(0));
EXPECT_TRUE(WriteFile(kName, &data, sizeof(data));
}
TEST(WriteFileTest, FailsIfOpenFails) {
MockFileIO fio;
static char data[] = "hello";
const char* kName = "test";
File test_file;
// Tell the mock to expect certain calls and what to
// return on those calls.
EXPECT_CALL(fio, Open(kName, "wb")
.WillOnce(Return(NULL));
EXPECT_FALSE(WriteFile(kName, &data, sizeof(data));
}
TEST(WriteFileTest, FailsIfWriteFails) {
MockFileIO fio;
static char data[] = "hello";
const char* kName = "test";
File test_file;
// Tell the mock to expect certain calls and what to
// return on those calls.
EXPECT_CALL(fio, Open(kName, "wb")
.WillOnce(Return(&test_file));
EXPECT_CALL(fio, Write(&data, 1, sizeof(data), &test_file))
.WillOnce(Return(0));
EXPECT_FALSE(WriteFile(kName, &data, sizeof(data));
}
TEST(WriteFileTest, FailsIfCloseFails) {
MockFileIO fio;
static char data[] = "hello";
const char* kName = "test";
File test_file;
// Tell the mock to expect certain calls and what to
// return on those calls.
EXPECT_CALL(fio, Open(kName, "wb")
.WillOnce(Return(&test_file));
EXPECT_CALL(fio, Write(&data, 1, sizeof(data), &test_file))
.WillOnce(Return(sizeof(data)));
EXPECT_CALL(file, Close(&test_file))
.WillOnce(Return(EOF));
EXPECT_FALSE(WriteFile(kName, &data, sizeof(data));
}
I didn't have to provide a test implementation of fopen/fwrite/fclose. googlemock handles this for me. You can make the mock strict if you want. A Strict mock will fail the tests if any function that is not expected is called or if any function that is expected is called with the wrong arguments. Googlemock provides a ton of helpers and adapters so you generally don't need to write much code to get the mock to do what you want. It takes a few days to learn the different adapters but if you're using it often they quickly become second nature.
Here's an example using FindFirstFile, FindNextFile, FindClose
First the interface
class FindFileInterface {
public:
virtual HANDLE FindFirstFile(
LPCTSTR lpFileName,
LPWIN32_FIND_DATA lpFindFileData) = 0;
virtual BOOL FindNextFile(
HANDLE hFindFile,
LPWIN32_FIND_DATA lpFindFileData) = 0;
virtual BOOL FindClose(
HANDLE hFindFile) = 0;
virtual DWORD GetLastError(void) = 0;
};
Then the actual implementation
class FindFileImpl : public FindFileInterface {
public:
virtual HANDLE FindFirstFile(
LPCTSTR lpFileName,
LPWIN32_FIND_DATA lpFindFileData) {
return ::FindFirstFile(lpFileName, lpFindFileData);
}
virtual BOOL FindNextFile(
HANDLE hFindFile,
LPWIN32_FIND_DATA lpFindFileData) {
return ::FindNextFile(hFindFile, lpFindFileData);
}
virtual BOOL FindClose(
HANDLE hFindFile) {
return ::FindClose(hFindFile);
}
virtual DWORD GetLastError(void) {
return ::GetLastError();
}
};
The Mock using gmock
class MockFindFile : public FindFileInterface {
public:
MOCK_METHOD2(FindFirstFile,
HANDLE(LPCTSTR lpFileName, LPWIN32_FIND_DATA lpFindFileData));
MOCK_METHOD2(FindNextFile,
BOOL(HANDLE hFindFile, LPWIN32_FIND_DATA lpFindFileData));
MOCK_METHOD1(FindClose, BOOL(HANDLE hFindFile));
MOCK_METHOD0(GetLastError, DWORD());
};
The function I need to test.
DWORD PrintListing(FindFileInterface* findFile, const TCHAR* path) {
WIN32_FIND_DATA ffd;
HANDLE hFind;
hFind = findFile->FindFirstFile(path, &ffd);
if (hFind == INVALID_HANDLE_VALUE)
{
printf ("FindFirstFile failed");
return 0;
}
do {
if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
_tprintf(TEXT(" %s \n"), ffd.cFileName);
} else {
LARGE_INTEGER filesize;
filesize.LowPart = ffd.nFileSizeLow;
filesize.HighPart = ffd.nFileSizeHigh;
_tprintf(TEXT(" %s %ld bytes\n"), ffd.cFileName, filesize.QuadPart);
}
} while(findFile->FindNextFile(hFind, &ffd) != 0);
DWORD dwError = findFile->GetLastError();
if (dwError != ERROR_NO_MORE_FILES) {
_tprintf(TEXT("error %d"), dwError);
}
findFile->FindClose(hFind);
return dwError;
}
The unit tests.
#include
#include
using ::testing::_;
using ::testing::Return;
using ::testing::DoAll;
using ::testing::SetArgumentPointee;
// Some data for unit tests.
static WIN32_FIND_DATA File1 = {
FILE_ATTRIBUTE_NORMAL, // DWORD dwFileAttributes;
{ 123, 0, }, // FILETIME ftCreationTime;
{ 123, 0, }, // FILETIME ftLastAccessTime;
{ 123, 0, }, // FILETIME ftLastWriteTime;
0, // DWORD nFileSizeHigh;
123, // DWORD nFileSizeLow;
0, // DWORD dwReserved0;
0, // DWORD dwReserved1;
{ TEXT("foo.txt") }, // TCHAR cFileName[MAX_PATH];
{ TEXT("foo.txt") }, // TCHAR cAlternateFileName[14];
};
static WIN32_FIND_DATA Dir1 = {
FILE_ATTRIBUTE_DIRECTORY, // DWORD dwFileAttributes;
{ 123, 0, }, // FILETIME ftCreationTime;
{ 123, 0, }, // FILETIME ftLastAccessTime;
{ 123, 0, }, // FILETIME ftLastWriteTime;
0, // DWORD nFileSizeHigh;
123, // DWORD nFileSizeLow;
0, // DWORD dwReserved0;
0, // DWORD dwReserved1;
{ TEXT("foo.dir") }, // TCHAR cFileName[MAX_PATH];
{ TEXT("foo.dir") }, // TCHAR cAlternateFileName[14];
};
TEST(PrintListingTest, TwoFiles) {
const TCHAR* kPath = TEXT("c:\\*");
const HANDLE kValidHandle = reinterpret_cast(1234);
MockFindFile ff;
EXPECT_CALL(ff, FindFirstFile(kPath, _))
.WillOnce(DoAll(SetArgumentPointee<1>(Dir1),
Return(kValidHandle)));
EXPECT_CALL(ff, FindNextFile(kValidHandle, _))
.WillOnce(DoAll(SetArgumentPointee<1>(File1),
Return(TRUE)))
.WillOnce(Return(FALSE));
EXPECT_CALL(ff, GetLastError())
.WillOnce(Return(ERROR_NO_MORE_FILES));
EXPECT_CALL(ff, FindClose(kValidHandle));
PrintListing(&ff, kPath);
}
TEST(PrintListingTest, OneFile) {
const TCHAR* kPath = TEXT("c:\\*");
const HANDLE kValidHandle = reinterpret_cast(1234);
MockFindFile ff;
EXPECT_CALL(ff, FindFirstFile(kPath, _))
.WillOnce(DoAll(SetArgumentPointee<1>(Dir1),
Return(kValidHandle)));
EXPECT_CALL(ff, FindNextFile(kValidHandle, _))
.WillOnce(Return(FALSE));
EXPECT_CALL(ff, GetLastError())
.WillOnce(Return(ERROR_NO_MORE_FILES));
EXPECT_CALL(ff, FindClose(kValidHandle));
PrintListing(&ff, kPath);
}
TEST(PrintListingTest, ZeroFiles) {
const TCHAR* kPath = TEXT("c:\\*");
MockFindFile ff;
EXPECT_CALL(ff, FindFirstFile(kPath, _))
.WillOnce(Return(INVALID_HANDLE_VALUE));
PrintListing(&ff, kPath);
}
TEST(PrintListingTest, Error) {
const TCHAR* kPath = TEXT("c:\\*");
const HANDLE kValidHandle = reinterpret_cast(1234);
MockFindFile ff;
EXPECT_CALL(ff, FindFirstFile(kPath, _))
.WillOnce(DoAll(SetArgumentPointee<1>(Dir1),
Return(kValidHandle)));
EXPECT_CALL(ff, FindNextFile(kValidHandle, _))
.WillOnce(Return(FALSE));
EXPECT_CALL(ff, GetLastError())
.WillOnce(Return(ERROR_ACCESS_DENIED));
EXPECT_CALL(ff, FindClose(kValidHandle));
PrintListing(&ff, kPath);
}
I didn't have to implement any of the mock functions.