我仍然有问题为自己辩护TDD。正如我在其他问题中提到的,我写的90%的代码绝对没有,只有
在TDD下处理代码需要处理的虚假数据所花费的时间令人难以置信 - 我花了5倍的时间来提供示例数据,就像编写应用程序代码一样。
这个问题的部分原因是我经常使用我没有经验的API进行编程,这迫使我编写小应用程序来向我展示真实API的行为,以便我可以在上面编写有效的假货/模拟那个API。首先编写实现与TDD相反,但在这种情况下它是不可避免的:我不知道真正的API如何表现,所以我怎么能够创建一个虚假的API实现而不玩它?
我已经阅读了几本关于这个主题的书籍,包括肯特贝克的测试驱动开发,例如,以及迈克尔·费瑟斯有效地使用遗留代码,这似乎是TDD狂热分子的福音。 Feathers的书与描述突破依赖关系的方式接近,但即使这样,提供的示例也有一个共同点:
我的程序不遵循该模式。相反,程序本身的唯一输入是它运行的系统。
如何在这样的项目中有效地使用TDD?在我实际使用该API之前,我已经将大部分API包装在C ++类中,但有时候是wrappers themselves can become quite complicated,并且值得进行自己的测试。
答案 0 :(得分:13)
请参阅下面的FindFirstFile / FindNextFile / FindClose示例
我使用googlemock。对于外部API,我通常会创建一个接口类。假设我要打电话给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);
}
};
然后使用googlemock我像这样制作一个MockFileIO类
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));
}
这使得编写测试变得容易。我不必提供Open / Write / Close的测试实现。 googlemock为我处理。就像在。(注意我使用googletest作为我的单元测试框架。)
假设我有一个需要测试的功能
// 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;
}
这是测试。
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));
}
我没有提供fopen / fwrite / fclose的测试实现。 googlemock为我处理这个问题。如果你愿意,你可以使mock严格。如果调用任何不期望的函数或者使用错误的参数调用任何预期的函数,则严格模拟将使测试失败。 Googlemock提供了大量的帮助程序和适配器,因此您通常不需要编写太多代码来让模拟执行您想要的操作。学习不同的适配器需要几天的时间,但如果你经常使用它们,它们很快就会变成第二天性。
以下是使用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());
};
我需要测试的功能。
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::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<HANDLE>(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<HANDLE>(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<HANDLE>(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);
}
我没有实现任何模拟功能。
答案 1 :(得分:1)
我认为单元测试瘦包装类是不可行的。包装器越厚,测试不直接命中API的位就越容易,因为包装器本身可以有多个层,其中最低层可以以某种方式进行模拟。
虽然你可以这样做:
// assuming Windows, sorry.
namespace Wrapper
{
std::string GetComputerName()
{
char name[MAX_CNAME_OR_SOMETHING];
::GetComputerName(name);
return std::string(name);
}
}
TEST(GetComputerName) // UnitTest++
{
CHECK_EQUAL(std::string(getenv("COMPUTERNAME")), Wrapper::GetComputerName());
}
我不知道像这样的测试带来了很多价值,并且倾向于让我的测试重点放在数据的转换而不是集合这样
答案 2 :(得分:-1)
修改我知道这不是您所需要的。我将它留在这里作为社区维基,因为评论很有用。
哈哈,好吧,每当我看到工作广告上写着:“需要测试驱动开发”或“敏捷开发方法”之类的东西,我就会用另一种方式运行。我严格认为,检查问题并了解解决问题的最佳方法(我是成对工作,还是定期与客户联络,或者只是针对硬件规范编写内容)是工作的一部分,并且不会需要一个奇特的名字,并强迫不需要它们的项目。咆哮。
我会说你至少不需要测试Windows API - 你正在测试无法修改的API函数。
如果您正在构建一个对Windows API调用的输出执行某些处理的函数,则可以对其进行测试。我们只是说,例如,您正在拉动窗口标题,并将它们反转。你不能测试GetWindowTitle和SetWindowTitle,但你可以测试你编写的InvertString,只需用“Thisisastring”调用你的函数并测试函数的结果是否为“gnirtsasisihT”。如果是,那么,更新矩阵中的测试分数。如果不是,哦,亲爱的,你所做的任何修改都破坏了程序,不好,请回去修复。
对于这样一个简单的功能,存在一个问题,即这是否真的有必要。测试是否可以防止任何漏洞入侵?算法多久会被更改等错误编译/破坏?
这样的测试在我所称的MPIR项目上更有用,MPIR是针对许多不同平台构建的。我们在每个平台上运行构建,然后测试结果二进制文件,以确保编译器没有通过优化创建错误,或者我们在编写算法时所做的事情并没有在该平台上做出意想不到的事情。这是一个检查,以确保我们不会错过任何事情。如果它通过,那很好,如果它失败了,有人会去看看为什么。
就个人而言,我不确定如何完全通过测试来驱动整个开发过程。毕竟,他们是检查。他们没有告诉你什么时候在你的代码库中做出重大改变,只是你所做的工作。所以,我要说TDD只是一个流行语。有人可以自由地反对我。