Как я могу эффективно протестировать Windows API?

У меня все еще есть проблемы, оправдывающие TDD для меня. Как я уже упоминал в других вопросах, 90% кода, который я пишу, абсолютно ничего, кроме

  • Вызов некоторых функций Windows API и
  • Распечатайте данные, возвращенные из указанных функций.

Время, затрачиваемое на поддельные данные, которые код должен обрабатывать в TDD, невероятно - я буквально трачу в 5 раз больше времени на получение данных примера, поскольку я бы потратил только написание кода приложения.

Часть этой проблемы заключается в том, что я часто программирую против API, с которыми у меня мало опыта, что заставляет меня писать небольшие приложения, которые показывают мне, как ведет себя настоящий API, чтобы я мог писать эффективные подделки /mocks поверх этот API. Написание реализации в первую очередь является противоположностью TDD, но в этом случае это неизбежно: я не знаю, как ведет себя настоящий API, так как я собираюсь создать фальшивую реализацию API, не играя с ним?

Я прочитал несколько книг по этому предмету, в том числе "Кент Бек", разработанный на основе тестирования, например, и "Эффективные действия Майкла Перса" с устаревшим кодом, который, похоже, является евангелием для фанатиков TDD. Книга "Перья" приближается к тому, как она описывает разрастающиеся зависимости, но даже в этом случае приведенные примеры имеют одну общую черту:

  • Программа под тестированием получает ввод от других частей тестируемой программы.

Мои программы не соответствуют этому шаблону. Вместо этого единственным входом самой программы является система, с которой она запускается.

Как эффективно использовать TDD для такого проекта? Я уже обертываю большую часть API внутри классов С++, прежде чем на самом деле использую этот API, но иногда сами обертки могут стать довольно сложными и заслуживают собственных тестов.

Ответ 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 обрабатывает это для меня. Вы можете сделать макет строгим, если хотите. Строгий макет провалит тесты, если вызвана какая-либо функция, которая не ожидается, или если какая-либо ожидаемая функция вызывается с неправильными аргументами. 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);
}

Мне не нужно было выполнять какие-либо макетные функции.

Ответ 2

Я не думаю, что это возможно для unit test классов тонкой оболочки. Чем толще ваша обертка, тем легче будет проверять биты, которые непосредственно не попадают в 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

Изменить Я понимаю, что это не то, что вам нужно. Я оставляю это здесь как сообщество wiki, так как комментарии полезны.

Ха-ха, ну, всякий раз, когда я вижу объявления о работе со словами: "требует разработки, основанной на тестах" или "гибких методологий разработки" и тому подобное, я запускаю другой путь. Я строго придерживаюсь мнения, что рассмотрение проблемы и понимание наилучшего способа ее решения (я работаю в паре или регулярно свяжусь с клиентом или просто пишу что-то против спецификации оборудования) является частью работы и не " t нужно причудливое имя и принуждение к проектам, которые им не нужны. Намерение.

Я бы сказал, что вам не нужно, по крайней мере, вам не нужно тестировать Windows API - вы тестируете функции для API, который вы никак не можете изменить.

Если вы создаете функцию, которая выполняет какой-либо процесс на выходе вызова Windows API, вы можете проверить это. Скажем, например, вы вытаскиваете заголовки окон, учитывая hWnd и инвертируя их. Вы не можете тестировать GetWindowTitle и SetWindowTitle, но вы можете протестировать InvertString, который вы написали, просто позвонив своей функции с помощью "Thisisastring" и проверив, является ли результат функции "gnirtsasisihT". Если это так, отлично, обновите тестовую оценку в матрице. Если это не так, о, дорогая, все изменения, которые вы сделали, нарушили программу, а не хорошо, вернитесь и исправьте.

Возникает вопрос, действительно ли это необходимо для такой простой функции. Имеет ли тест, предотвращающий проникновение ошибок? Как часто алгоритм может быть неправильно скомбинирован/нарушен изменениями и т.д.?

Такие тесты более полезны в проекте, на котором я работаю, называемом MPIR, который построен на многих разных платформах. Мы запускаем сборку на каждой из этих платформ, затем проверяем результирующий двоичный файл, чтобы убедиться, что компилятор не создал ошибку с помощью оптимизации, или что-то, что мы сделали с написанием алгоритма, не создает неожиданных вещей на этой платформе. Это проверка, чтобы мы не пропустили вещи. Если это пройдет, здорово, если это не удастся, кто-то пойдёт и посмотрит, почему.

Лично я не уверен точно, как весь процесс разработки может быть вызван только тестами. В конце концов, это проверки. Они не говорят вам, когда пришло время внести существенные изменения в направление в вашей кодовой базе, только то, что вы сделали, работает. Итак, я собираюсь зайти так далеко, чтобы сказать, что TDD - просто модное слово. Кто-то может не согласиться со мной.