How can I effectively test against the Windows API?

后端 未结 3 905
耶瑟儿~
耶瑟儿~ 2021-02-01 21:55

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

  1. Call some Windows AP
3条回答
  •  无人及你
    2021-02-01 22:20

    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.

提交回复
热议问题