外网拷贝,侵删
单元测试是编写测试单个“单元”代码的测试的过程。这与一起测试多段代码的集成测试相反。经常出现的问题是,您想要为某些代码 A 编写单元测试,但该代码调用了其他一些代码 B,并且您不想在单元测试中测试 B。B 应该有自己的单元测试,否则 B 就像操作系统 API 一样不受您的控制。A 的单元测试应该只测试 A 是否执行了您期望的操作,前提是它调用的代码执行了预期的操作。
常见的解决方案是将 B 变成一个接口并提供两个实现。真实的实现和模拟的实现。在 A 的单元测试中,您使用 B 的模拟实现。该解决方案的问题是编写 B 的模拟实现可能需要大量工作。这些工作本身可能是错误的根源,并且会影响您的工作日程。
因此,人们编写了模拟库或模拟框架来帮助更轻松地编写这些模拟类。我只有Google 的 C++ 模拟库googlemock或gmock的经验,但是通过阅读网上有关其他模拟库的信息,我得到的印象是googlemock是最好的库之一,所以我想我很幸运这是我第一次使用模拟库。
举一个简单的例子,假设您有一些调用 fopen、fwrite 和 fclose 的代码。
// Writes a file, returns true on success.
bool WriteFile(const char* filename, const void* data, size_size) {
FILE* file = fopen(filename, "wb");
if (!file) {
return false;
}
if (fwrite(data, 1, size, file) != size) {
fclose(fp);
return false;
}
if (fclose(file) != 0) {
return false;
}
return true;
}
我们需要做的第一件事就是更改代码以使用接口,而不是直接调用 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;
};
这是该类的实际实现
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);
}
};
这是重构为通过界面工作的原始函数。
// 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;
}
接下来我们使用 googlemock 库创建相同界面的 Mock 版本。我们将在单元测试中使用这个模拟版本。
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));
};
googlemock 库基本上提供了一个框架,因此我们可以检查进入模拟类的所有调用,并决定要做什么、要返回什么值或要发生什么副作用。
这是使用我们刚刚创建的模拟进行的单元测试。我不必提供打开/写入/关闭的测试实现。googlemock 为我处理这个问题。
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(&fio, 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(&fio, 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(&fio, 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(&fio, kName, &data, sizeof(data)));
}
请注意,我不必提供 fopen/fwrite/fclose 的模拟实现。googlemock 为我处理这个问题。
如果您愿意,您可以使模拟变得严格。如果调用任何不期望的函数或者使用错误的参数调用任何期望的函数,则严格模拟将使测试失败。Googlemock 提供了大量的帮助器和适配器,因此您通常不需要编写太多代码即可让模拟执行您想要的操作。学习不同的适配器需要几天的时间,但如果您经常使用它,它们很快就会成为第二天性。
有人提到 fopen/fwrite/fclose 太简单了,并要求提供更好的示例。我让他们给 api 命名,他们说 FindFirstFile、FindNextFile、FindClose 怎么样。
下面是使用 FindFirstFile、FindNextFile、FindClose 的示例。
首先是界面
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;
};
然后实际执行
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();
}
};
用 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());
};
我们想要测试的函数已经重构为使用 FindFileInterface。
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 <DIR>\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;
}
使用模拟进行单元测试。
#include <gtest/gtest.h>
#include <gmock/gmock.h>
using ::testing::_;
using ::testing::Return;
using ::testing:&#128515;oAll;
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<HANDLE>(1234);
MockFindFile ff;
// Tell the mock what calls to expect and what to do with each one.
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<HANDLE>(1234);
MockFindFile ff;
// Tell the mock what calls to expect and what to do with each one.
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;
// Tell the mock what calls to expect and what to do with each one.
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<HANDLE>(1234);
MockFindFile ff;
// Tell the mock what calls to expect and what to do with each one.
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);
}
请注意,我不必实现任何模拟函数。
虽然模拟与单元测试密切相关,但它们不是同一件事。Google 有一个单元测试库 googletest 或 gtest。您可以将 googlemock 与 googletest 一起使用,也可以将其与任何测试框架一起使用。