C ++ Высокопроизводительное модульное тестирование с помощью Google Mock?

Я использую Google Mock и пытаюсь смоделировать системные вызовы C ++ (в частности, функции C ++ 11 chrono).

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

Каков наиболее эффективный и эффективный способ включения системных вызовов в Google Mock?

4

Решение

Нет, вам не нужно прибегать к ложным статическим классам — это один из многих вариантов.

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

Предположим для простоты, что вы хотите смоделировать функцию в std::this_thread пространство имен, как sleep_for(std::milliseconds),

Пример 0 — непроверяемая базовая линия

Без насмешек, давайте предположим, что ваш код выглядит так:

class untestable_class
{
public:

void some_function()
{
if (must_sleep())
{
auto sleep_duration = std::chrono::milliseconds(1000);
std::this_thread::sleep_for(sleep_duration);
}
}
};

Вы бы использовали этот класс так:

void use_untestable_class()
{
untestable_class instance;
instance.some_function();
}

Из-за зависимости от стандартной библиотеки sleep_for функция, у вас есть зависимость платформы, которая делает some_function трудно провести модульное тестирование, не выполняя из него интеграционный тест.

Пример 1 — Тестирование с использованием статической политики

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

в статический У нас есть одна «реальная» политика, которая зависит от платформы:

struct system_thread_policy1
{
static void sleep_milliseconds(long milliseconds)
{
auto sleep_duration = std::chrono::milliseconds(milliseconds);
std::this_thread::sleep_for(sleep_duration);
}
};

У нас также есть «фиктивная» политика, которую мы можем контролировать в модульных тестах:

struct mock_thread_policy1
{
// Mock attributes to verify interactions.
static size_t sleep_milliseconds_count;
static size_t sleep_milliseconds_arg1;

// Resets the mock attributes before usage.
static void sleep_milliseconds_reset()
{
sleep_milliseconds_count = 0;
sleep_milliseconds_arg1 = 0;
}

static void sleep_milliseconds(size_t milliseconds)
{
sleep_milliseconds_count++;
sleep_milliseconds_arg1 = milliseconds;
}
};

// This is needed with MS compilers to keep all mock code in a header file.
__declspec(selectany) size_t mock_thread_policy1::sleep_milliseconds_count;
__declspec(selectany) size_t mock_thread_policy1::sleep_milliseconds_arg1;

Производственный класс, использующий политику, принимает тип политики в качестве параметра шаблона и вызывает его sleep_milliseconds статически:

template <typename thread_policy>
class testable_class1
{
public:

void some_function()
{
if (must_sleep())
{
thread_policy::sleep_milliseconds(sleep_duration_milliseconds);
}
}

private:

enum { sleep_duration_milliseconds = 1000 };
};

В производственном коде testable_class1 создается с использованием «реальной» политики:

void use_testable_class1()
{
testable_class1<system_thread_policy1> instance;
instance.some_function();
}

В модульном тесте testable_class1 создается с использованием политики «mock»:

void test_testable_class1()
{
mock_thread_policy1::sleep_milliseconds_reset();
testable_class1<mock_thread_policy1> instance;
instance.some_function();

assert(mock_thread_policy1::sleep_milliseconds_count == 1);
assert(mock_thread_policy1::sleep_milliseconds_arg1 == 1000);
//assert("some observable behavior on instance");
}

Недостатки этого метода:

  • Функции для проверки взаимодействия, такие как количество звонков а также проверка аргументов выше, может быть добавлен к макету и использоваться для проверки модульных тестов взаимодействия классов.
  • Статический вызов позволяет оптимизатору легко встроить «реальный» вызов sleep_for,

Недостатки этого метода:

  • Статическое состояние добавляет шум к макету.
  • Статическое состояние должно быть сброшено в каждом модульном тесте, где он используется, поскольку различные модульные тесты изменят это липкое состояние.
  • Статическое состояние делает невозможным надежное использование макета, если тестовый исполнитель паралеллизирует модульные тесты, поскольку разные потоки будут играть в одном и том же состоянии, вызывая непредсказуемое поведение.

Пример 2 — Тестируемый с использованием политики экземпляров

в пример У нас есть одна «реальная» политика, которая зависит от платформы:

struct system_thread_policy2
{
void sleep_milliseconds(size_t milliseconds) const
{
auto sleep_duration = std::chrono::milliseconds(milliseconds);
std::this_thread::sleep_for(sleep_duration);
}
};

У нас также есть «фиктивная» политика, которую мы можем контролировать в модульных тестах:

struct mock_thread_policy2
{
mutable size_t sleep_milliseconds_count;
mutable size_t sleep_milliseconds_arg1;

mock_thread_policy2()
: sleep_milliseconds_count(0)
, sleep_milliseconds_arg1(0)
{
}

void sleep_milliseconds(size_t milliseconds) const
{
sleep_milliseconds_count++;
sleep_milliseconds_arg1 = milliseconds;
}
};

Производственный класс, использующий политику, принимает тип политики в качестве параметра шаблона, получает экземпляр политики, введенный в конструктор, и вызывает его sleep_milliseconds:

template <typename thread_policy>
class testable_class2
{
public:

testable_class2(const thread_policy& policy = thread_policy()) : m_thread_policy(policy) { }

void some_function() const
{
if (must_sleep())
{
m_thread_policy.sleep_milliseconds(sleep_duration_milliseconds);
}
}

private:

// Needed since the thread policy is taken as a reference.
testable_class2(const testable_class2&);
testable_class2& operator=(const testable_class2&);

enum { sleep_duration_milliseconds = 1000 };

const thread_policy& m_thread_policy;
};

В производственном коде testable_class2 создается с использованием «реальной» политики:

void use_testable_class2()
{
const testable_class2<system_thread_policy2> instance;
instance.some_function();
}

В модульном тесте testable_class2 создается с использованием политики «mock»:

void test_testable_class2()
{
mock_thread_policy2 thread_policy;
const testable_class2<mock_thread_policy2> instance(thread_policy);
instance.some_function();

assert(thread_policy.sleep_milliseconds_count == 1);
assert(thread_policy.sleep_milliseconds_arg1 == 1000);
//assert("some observable behavior on instance");
}

Недостатки этого метода:

  • Функции для проверки взаимодействия, такие как количество звонков а также проверка аргументов выше, может быть добавлен к макету и использоваться для проверки модульных тестов взаимодействия классов.
  • Вызов экземпляра позволяет оптимизатору легко встроить «настоящий» вызов sleep_for,
    • В макетах нет статического состояния, что облегчает написание, чтение и сопровождение модульных тестов.

Недостатки этого метода:

  • Состояние экземпляра добавляет изменяемый шум к макету.
  • Состояние экземпляра добавляет шум клиенту (testable_class2) — если взаимодействия не нуждаются в проверке, политику можно передать по значению в конструкторе, и большая часть класса goo исчезнет.

Пример 3 — Тестирование с использованием виртуальной политики

Это отличается от первых двух примеров тем, что это зависит от виртуальной диспетчеризации, но оставляет вероятную возможность для компилятора / компоновщика оптимизировать виртуальную диспетчеризацию, если он может обнаружить, что эксплуатируемый экземпляр имеет базовый тип.

Во-первых, у нас есть производство база класс, который использует «реальную» политику в не чистой виртуальной функции:

class testable_class3
{
public:

void some_function()
{
if (must_sleep())
{
sleep_milliseconds(sleep_duration_milliseconds);
}
}

private:

virtual void sleep_milliseconds(size_t milliseconds)
{
auto sleep_duration = std::chrono::milliseconds(milliseconds);
std::this_thread::sleep_for(sleep_duration);
}

enum { sleep_duration_milliseconds = 1000 };
};

Во-вторых, у нас есть полученный класс, который реализует «виртуальную» политику в виртуальной функции (своего рода шаблон проектирования Template Template):

class mock_testable_class3 : public testable_class3
{
public:

size_t sleep_milliseconds_count;
size_t sleep_milliseconds_arg1;

mock_testable_class3()
: sleep_milliseconds_count(0)
, sleep_milliseconds_arg1(0)
{
}

private:

virtual void sleep_milliseconds(size_t milliseconds)
{
sleep_milliseconds_count++;
sleep_milliseconds_arg1 = milliseconds;
}
};

В производственном коде testable_class3 просто создан как сам по себе:

void use_testable_class3()
{
// Lots of opportunities to optimize away the virtual dispatch.
testable_class3 instance;
instance.some_function();
}

В модульном тесте testable_class3 создается с использованием производного класса «mock»:

void test_testable_class3()
{
mock_testable_class3 mock_instance;
auto test_function = [](testable_class3& instance) { instance.some_function(); };
test_function(mock_instance);

assert(mock_instance.sleep_milliseconds_count == 1);
assert(mock_instance.sleep_milliseconds_arg1 == 1000);
//assert("some observable behavior on mock_instance");
}

Недостатки этого метода:

  • Функции для проверки взаимодействия, такие как количество звонков а также проверка аргументов выше, может быть добавлен к макету и использоваться для проверки модульных тестов взаимодействия классов.
  • Виртуальный вызов базового класса для «самого себя» позволяет оптимизатору встроить «настоящий» вызов sleep_for,
  • В макетах нет статического состояния, что облегчает написание, чтение и сопровождение модульных тестов.

Недостатки этого метода:

  • Базовый класс не может быть отмечен final (C ++ 11), поскольку он должен быть разрешен для наследования, и это может повлиять на остальную часть дизайна класса, если сложность выше, чем в простом примере выше.
  • Компилятор / компоновщик может быть низкого уровня или просто не может оптимизировать виртуальную диспетчеризацию.

Тестовый забег

Все вышеперечисленное можно проверить с помощью этого:

int _tmain(int argc, _TCHAR* argv[])
{
test_testable_class1();
test_testable_class2();
test_testable_class3();

return 0;
}

и полный работоспособный пример в http://pastebin.com/0qJaQVcD

4

Другие решения

Прежде всего, виртуальная рассылка не так уж дорога, поэтому вы можете заниматься микрооптимизацией. Даже для встроенной платформы.

Если вы действительно хотите избежать виртуальной отправки, вы можете использовать статический полиморфизм с шаблонами и внедрять системные вызовы через параметр шаблона. Что-то вроде этого :

struct SysCallCaller
{
static void doCall()
{
// really execute system call
}
private:
SysCallCaller();
};

struct SysCallMock
{
static void doCall()
{
// mock system call
}
};

template < typename SysCallType >
struct MyClass
{
void Foo()
{
SysCallType::doCall();
}

};

int main()
{
#ifdef CALL_MOCK
MyClass< SysCallMock > obj;
#else
MyClass< SysCallCaller > obj;
#endif

obj.Foo();
}

Прежде всего, старайтесь избегать одиночек. Это просто скрытые глобальные переменные.

0

По вопросам рекламы [email protected]