如何有效地测试Windows API?

Bil*_*eal 17 c++ tdd winapi

我仍然有问题为自己辩护TDD.正如我在其他问题中提到的,我写的90%的代码绝对没有

  1. 调用一些Windows API函数和
  2. 打印出从所述函数返回的数据.

提出代码需要在TDD下处理的假数据所花费的时间是令人难以置信的 - 我花了5倍的时间来提供示例数据,就像编写应用程序代码一样.

这个问题的部分原因是我经常使用我没有经验的API编程,这迫使我编写小应用程序来向我展示真实API的行为,以便我可以在API之上编写有效的假货/模拟.首先编写实现与TDD相反,但在这种情况下它是不可避免的:我不知道真正的API如何表现,那么我怎么能够创建一个虚假的API实现而不用它呢?

我已经阅读了几本关于这个主题的书,包括Kent Beck的测试驱动开发,例如,以及Michael Feathers的"有效地使用遗留代码",这似乎是TDD狂热分子的福音.Feathers的书与描述破坏依赖关系的方式接近,但即使这样,提供的示例也有一个共同点:

  • 被测程序从被测程序的其他部分获得输入.

我的程序不遵循这种模式.相反,程序本身的唯一输入是它运行的系统.

如何在这样的项目中有效地使用TDD?在实际使用该API之前,我已经将大部分API包装在C++类中,但有时包装器本身会变得非常复杂,并且值得进行自己的测试.

gma*_*man 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;
};
Run Code Online (Sandbox Code Playgroud)

实际的实施就是这样

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);
  }
};
Run Code Online (Sandbox Code Playgroud)

然后使用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));
}
Run Code Online (Sandbox Code Playgroud)

这使得编写测试变得容易.我不必提供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;
}
Run Code Online (Sandbox Code Playgroud)

这是测试.

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));
}
Run Code Online (Sandbox Code Playgroud)

我没有提供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;
};
Run Code Online (Sandbox Code Playgroud)

然后实际执行

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();
  }
};
Run Code Online (Sandbox Code Playgroud)

模拟使用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());
};
Run Code Online (Sandbox Code Playgroud)

我需要测试的功能.

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;
}
Run Code Online (Sandbox Code Playgroud)

单元测试.

#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);
}
Run Code Online (Sandbox Code Playgroud)

我没有必要实现任何模拟函数.

  • 选择一个特定的Windows API函数进行讨论. (2认同)