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

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

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

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

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

Я прочитал несколько книг по предмету, включая Разработку через тестирование Kent Beck, Примером, и Michael Feathers, Рабочий Эффективно с Унаследованным кодом, которые, кажется, евангелие для фанатиков TDD. Книга Feathers приближается в способе, которым она описывает вспыхивающий зависимости, но даже затем, обеспеченные примеры имеют одну общую черту:

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

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

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

17
задан Community 23 May 2017 в 12:32
поделиться

3 ответа

См. Ниже пример 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);
}

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

13
ответ дан 30 November 2019 в 14:25
поделиться

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

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

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

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

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

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

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

-1
ответ дан 30 November 2019 в 14:25
поделиться

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

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

1
ответ дан 30 November 2019 в 14:25
поделиться
Другие вопросы по тегам:

Похожие вопросы: