Как я могу эффективно протестировать с Windows API? - PullRequest
17 голосов
/ 10 апреля 2010

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

  1. вызов некоторых функций Windows API и
  2. Распечатайте данные, возвращенные этими функциями.

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

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

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

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

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

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

Ответы [ 3 ]

13 голосов
/ 12 апреля 2010

См. Ниже для 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);
}

Мне не нужно было реализовывать какие-либо фиктивные функции.

1 голос
/ 10 апреля 2010

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

Я не знаю, что подобные тесты приносят большую пользу, и я склонен сосредоточить свое внимание на преобразовании данных, а не на коллекции таких.

0 голосов
/ 10 апреля 2010

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

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

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

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...