【问题标题】:How can I effectively test against the Windows API?如何有效地针对 Windows API 进行测试?
【发布时间】:2011-02-06 08:45:36
【问题描述】:

我仍然无法为自己证明 TDD 的合理性。正如我在其他问题中提到的那样,我编写的 90% 的代码完全没有任何作用

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

想出代码需要在 TDD 下处理的假数据所花费的时间令人难以置信——我花在想出示例数据上的时间实际上是编写应用程序代码所花费的时间的 5 倍。

这个问题的部分原因是我经常针对我没有经验的 API 进行编程,这迫使我编写小型应用程序来向我展示真实 API 的行为,以便我可以在上面编写有效的假冒/模拟那个API。首先编写实现与 TDD 是相反的,但在这种情况下这是不可避免的:我不知道真正的 API 是如何表现的,那么我到底要如何在不使用它的情况下创建 API 的假实现呢?

我已经阅读了几本关于该主题的书籍,包括 Kent Beck 的《Test Driven Development, By Example》和 Michael Feathers 的《有效地使用遗留代码》,这似乎是 TDD 狂热者的福音。 Feathers 的书在描述打破依赖关系的方式上非常接近,但即便如此,提供的示例也有一个共同点:

  • 被测程序从被测程序的其他部分获取输入。

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

如何在这样的项目中有效地使用 TDD?在我实际使用该 API 之前,我已经将大部分 API 封装在 C++ 类中,但有时是 wrappers themselves can become quite complicated,应该得到他们自己的测试。

【问题讨论】:

  • 这听起来像是“如果我唯一的工具是锤子,那么每个问题看起来都像钉子”。也许您可以在使用 API 时忽略 TDD,当您对它的工作原理有某种感觉时,将其包装在接口中并为 API 提供模型实现以用于测试目的,并在使用该接口的应用程序上执行 TDD .
  • 我以前也遇到过同样的情况,没有好的解决办法。但是,至少即使您先编写实现,然后根据实现的行为方式编写测试(可以通过代码生成自动化),您也正在创建一个安全网,如果您稍后更改某些内容,则会警告您打破原有的行为。如果您有很多代码重用,但发现自己必须更新可重用代码以使其适用于新场景,这非常有用。现有的单元测试将确保您的“改进”不会破坏任何东西。
  • @Laserallan:这就是我正在做的事情。我没有在小型一次性测试程序上使用 TDD 的问题。我确实有一个问题,不得不编写大量的一次性测试应用程序。
  • @AaronLS:这就是我测试报告生成的方式。但是当我花时间包装我正在使用的 API 时,我想把它变成一个 C++ 风格的 API。例如,stackoverflow.com/questions/2531874/…。 TDD 对日志记录代码很好,但包装器有时可能很复杂,需要自己进行测试。
  • @Billy ONeal:我好像没抓住重点。所以你问的问题基本上是有没有一种使用 TDD 来理解 API 的方法?理解 API 的传统方法是真正通读文档还是编写测试应用程序,直到您对它的工作原理有所了解?

标签: c++ winapi tdd


【解决方案1】:

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 为我处理这个。如果你愿意,你可以使模拟严格。如果调用了任何不期望的函数或者使用错误的参数调用了任何期望的函数,则 Strict 模拟将失败。 Googlemock 提供了大量的帮助器和适配器,因此您通常不需要编写太多代码来让 mock 做您想做的事情。学习不同的适配器需要几天时间,但如果您经常使用它,它们很快就会成为第二天性。


这是一个使用 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);
}

我不必实现任何模拟函数。

【讨论】:

  • 问题在于没有设置存根。问题是Win32返回的数据结构比较复杂,这些结构的测试数据需要很长的时间来弥补。那是因为我事先不知道那个结构是什么样子的。
  • 选择一个特定的 windows API 函数进行讨论。
  • FindFirstFile/FindNextFile/FindClose 怎么样?
  • 这是一个很好的例子(因为它会变得非常复杂,呵呵)。您希望根据这些调用的结果向您的应用程序提供哪些数据?此外 - 如果您的“内部”实现挂在迭代器细节上并且没有公开它们,那么测试在这里可能不那么重要,最好花在这个返回状态的客户端上。
  • @dash-tom-bang: 是的——但是如果迭代器实现很复杂,它值得自己进行测试。这就是我解决问题的方式(向下滚动到我发布代码的编辑):stackoverflow.com/questions/2531874/…——但这对我来说似乎不是好的代码。有一些 API 示例需要模拟 5 或 6 个函数,例如服务控制管理器——它们要复杂得多。我想知道是否有更好的方法。
【解决方案2】:

我认为对瘦包装类进行单元测试是不可行的。包装器越厚,测试不直接命中 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());
}

我不知道这样的测试会带来很多价值,并且倾向于让我的测试专注于数据的转换,而不是收集这样的。

【讨论】:

  • 实际的模拟步骤并不困难。它正在创建复杂的测试数据以实际从模拟中返回。
  • 对 - 我不是想演示一个模拟,而是举一个函数测试的例子,并以此为例说明为什么我认为测试一个可能经过良好测试的第 3 方 API 是没有好好利用时间。
  • 嗯.. 问题是一些包装纸并不完全薄。 +1 花时间写出答案。
  • 对 - 这就是为什么我认为测试数据的转换可能比测试您获得的数据是否良好更容易。 IE。 “厚”包装意味着您所做的不仅仅是查询 API。尽管这可能达到了您的重点,因为填写这些结构至少可以说是令人恼火的。不过,我怀疑,您越能将转换提炼成单个工作单元,测试路径就会变得越明显。
【解决方案3】:

编辑我知道这不是您需要的。因为 cmets 很有用,所以我将其作为社区 wiki 留在这里。

哈哈,好吧,每当我看到带有“需要测试驱动开发”或“敏捷开发方法”之类的招聘广告时,我都会反其道而行之。我严格认为检查问题并了解解决问题的最佳方法(我是结对工作,还是定期与客户联络,或者只是根据硬件规范编写一些东西)是工作的一部分,并且不会不需要一个花哨的名字并强迫不需要它们的项目。吐槽一下。

我会说你不需要,至少你不需要测试 Windows API - 你正在测试一个你无论如何都不能修改的 API 函数。

如果您正在构建一个对 Windows API 调用的输出执行某些处理的函数,您可以对其进行测试。例如,假设您正在拉取给定 hWnd 的窗口标题并反转它们。您无法测试 GetWindowTitle 和 SetWindowTitle,但您可以测试您编写的 InvertString,只需使用“Thisisastring”调用您的函数并测试该函数的结果是否为“gnirtsasisihT”。如果是,那太好了,更新矩阵中的测试分数。如果不是,哦,亲爱的,你所做的任何修改都破坏了程序,不好,回去修复。

对于这样一个简单的功能,这是否真的有必要存在一个问题。进行测试是否可以防止任何错误潜入?该算法多久可能因更改等而被错误编译/破坏?

这样的测试在我工作的名为 MPIR 的项目中更有用,该项目针对许多不同的平台构建。我们在每个平台上运行构建,然后测试生成的二进制文件,以确保编译器没有通过优化产生错误,或者我们在编写算法时所做的事情不会在那个平台上做意外的事情。这是一张支票,以确保我们不会错过任何东西。如果通过了,很好,如果失败了,有人会去调查原因。

就个人而言,我不确定整个开发过程如何完全由测试驱动。毕竟,它们是支票。他们不会告诉你什么时候应该对代码库的方向做出重大改变,只是你所做的工作有效。所以,我要说 TDD 只是一个流行词。有人可以不同意我的观点。

【讨论】:

  • 我真正要寻找的不是 TDD 本身。更多的是测试本身。我刚刚发现,当我设计一个单片系统然后尝试对其进行检测以供以后测试时,它需要更长的时间。也许我写的问题很糟糕。
  • 我倾向于按位构建东西。那是我的测试。所以,如果我想获得一个窗口标题,我会构建一个函数来完成它,给定一个部分名称(或我需要的任何通用功能),然后编写一个小程序来检查它是否有效。如果确实如此,那就太好了,将其集成到代码库的其余部分中。也就是说,我也可以在代码的某些部分运行像 valgrind 这样的工具。我认为这正是您要避免的?
猜你喜欢
  • 2011-03-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-10-24
  • 2020-04-09
  • 1970-01-01
  • 2021-06-26
相关资源
最近更新 更多