Модульное тестирование  (unit testing) в первую очередь помогает нам получить код, который в будущем "не страшно" будет изменить: внести новую функциональность, переписать ради повышения производительности, исправить ошибки. Разработка таких тестов  - прямая обязанность авторов кода, а сам процесс - это не что иное, как программирование. И, зачастую, разработка модульных тестов сопоставима и по времени и по сложности с разработкой тестируемого кода. Особенно, когда "дизайн" программы плохо продуман (например, запутанные связи между классами). 

Цель модульного тестирования - проверить отдельные "единицы" программы, например, методы класса. При этом тест не должен "выходить за рамки" этих методов. Но это проще сказать, чем сделать.

Классы, очень часто используют в своей реализации другие классы, а те в свою очередь включают еще множество классов. Так как практически все программы используют библиотеки и различные платформы - подобная ситуация закономерна. Редкая функция изолирована полностью от "остального мира" - но этот мир не должен охватываться одним модульным тестом!

Возникает вопрос: как в этом случае писать тесты, как абстрагироваться от зависимостей? Ответ: использовать mock-объекты. Данная статья включает краткий обзор применения mock-объектов в тестировании с использованием библиотеки Google C++ Mocking Framework.

Инструмент

Условимся, что читатель использует одну из библиотек на С++, предназначенную для написания модульных тестов, или хотя бы знаком с процессом разработки таких тестов. Если же нет, то вот хорошая статья, в которой рассматриваются некоторые популярные библиотеки. Правда, в этот обзор не вошла библиотека "Google C++ Testing Framework" (далее просто Google Test). Почему? Возможно, потому что Google Test разрабатывается с 2008 года, а статья написана в 2004. 

Назначение Google Test тоже, что и у Boost.Test или CppUnit - разработка модульных тестов. Любая из этих библиотек прекрасно справляется с данной задачей. Однако, в состав Google Test входит особое дополнение, которое называется "Google C++ Mocking Framework" (конкретно в данной статье не делается существенных различий между понятиями "фреймворк" и "библиотека" - прошу извинить).  Google C++ Mocking Framework (далее просто Google Mock) не может работать отдельно от Google Test, поэтому если соберетесь использовать этот инструмент, придется собрать оба проекта. Это не сложно. В целом по проекту есть хорошая документация (и на русском тоже), около 800 вопросов с тегом Google Test на stackoverflow, достаточное количество статей. Мы будем рассматривать Google Mock, который позволяет создавать и задействовать в модульных тестах mock-объекты.

Чтобы понять, зачем нужны mock-объекты в модульно тестировании, нам понадобится простой пример.

Задача

Представим, что мы разрабатываем класс под названием Order (Заказ пользователя), и в нём есть несколько функций-членов: check - для проверки заказа, payment - для оплаты заказа, cancel - для отмены и еще много всего, что может быть связано с заказом. Допустим, что реализация методов очень даже нетривиальная. Модульные тесты должны проверить - корректно ли отрабатывают данные функции на различном наборе входных данных. Задача стандартная. Однако, есть нюанс...

Внутри реализации методов класса Order, используется объект класса Database (База данных). Этот класс инкапсулирует общение с неким хранилищем данных, и каждая операция из Order, инициирует обращение к этому хранилищу. Класс Database выполняет неизвестную нам "магию", требует "живую" базу данных, и вообще проектируется не нами, а другим отделом. Наши модульные тесты для Order не должны проверять еще и класс Database. Более того, нет никакого желания ради тестов настраивать настоящую базу данных. Поэтому от реального класса Database нужно избавиться - заменить его на некую "пустышку", пародию, которая имеет точно такой же интерфейс как и Database, но в действительности ничего не делает или делает, то что нужно нам в тесте.

Примечание: возможно Вы раскритикуете подобную связь между классами Order и Database, но поймите правильно - это только пример.

Ниже условно представлены классы, о которых идет речь: Database и Order.

Класс Order может работать с разными реализациями Database, а раз нам нужно сделать "пустышку", то почему бы не реализовать это самим - не прибегая к помощи Google Mock. Просто создадим свой класс DatabaseDummy, с открытым интерфейсом как у Database, и будем использовать его в тестах. Это логичная идея, но она не подходит для тестирования. И вот почему:

1) класс DatabaseDummy нужно разрабатывать. Да - это всего лишь эмуляция, но даже её утомительно писать. Можно где-то "посадить" ошибку - это нудный и бесполезный процесс.

2) класс DatabaseDummy не просто должен что-то делать, он должен уметь демонстрировать различное поведение, так будто он настоящий Database. А это поведение должно легко описываться в конкретном тесте. Другими словами, класс DatabaseDummy должен вести себя не как "пустышка", которая всегда отвечает одинаково. А этого сложнее добиться.

Описанный класс DatabaseDummy - это fake-класс, но не mock-класс. А у этих понятий разный смысл. Можно конечно довести класс DatabaseDummy до того состояния, когда он удовлетворит нашим требованиям, но в итоге мы получим дополнительный код, который также нужно тестировать (вот блин). А если у Вас, кроме Database, в реализации задействованы еще и другие объекты, например для работы с сетью? Сколько придется потратить времени, сколько мы еще напишем кода? Если же задействовать Google Mock, то всё станет намного проще. Google Mock предоставит нам готовые mock-классы с минимальными затратами сил с нашей стороны.

Решение

Как будет выглядеть полноценный управляемый mock-класс для класса Database, если доверить это Google Mock? Вот так:

Если Вы что-то напишите неправильно, то получите ошибку на этапе компиляции, и это очень хорошо.

В документации описано, как создавать mock-классы для разных случаев. Например, если используются не шаблоны, а наследование. Но на деле, процесс обучения созданию mock-классов с помощью Google Mock занимает несколько минут. На много интереснее затем использовать их в тестах, так как их поведение полностью подчинено нам. Кроме управления mock-объектами, можно (и даже нужно) указать их предполагаемое использование - сколько раз за тест метод должен вызываться, какие аргументы ему должны быть переданы. Если что-то из заданных предположений не выполнится, тест не будет считаться пройденным. Перейдем к простому примеру. 

Протестируем метод check из класса Order. Вместо Database будем использовать MockDatabase. Проверим, что метод make_query класса MockDatabase (а следовательно и класса Database), будет вызван за тест только один раз. При этом значение первого передаваемого аргумента должно быть равно "query", а второй аргумент, в момент вызова make_query, нас не интересует. Однако, в результате вызова make_query, второй аргумент должен быть установлен в "result", а сам метод должен вернуть true.

Результат

На первый взгляд выражение EXPECT_CALL кажется сложным и перегруженным. Однако, нам удалось одной строкой описать поведение метода make_query из класса Database (MockDatabase) в данном конкретном тесте. Кроме поведения, мы определили ряд предположений относительно того, как будет происходить использование метода make_query: количество вызовов, значения передаваемых аргументов. И всё в одной строке. На самом деле реальный тест имел бы множество EXPECT_CALL выражений, описывал бы и другие методы класса Database, определял бы последовательность их вызовов.

Возможности Google Mock в описании предполагаемого поведения mock-объектов очень обширны. SetArgRefereeDoAllReturn - это функции из библиотеки Google Mock. Полный список их велик, но благодаря им можно задавать очень сложное поведение для своих mock-объектов. Однако, во многих случаях вполне хватит и 10% того, что есть у Google Mock. 

Обратите внимание, что поведение не "зашито" в mock-объекте, а задается в начале очередного теста. Это позволяет гибко настраивать каждый отдельный тест. Например, в следующем примере мы ожидаем, что метод make_query вообще не будет вызван:

Основная мысль заключается в том, что используя mock-объекты, мы сосредотачиваемся не на их реализации, а на их поведении, и ожиданиях, относительно использования в тесте. Мы как бы описываем "внешнюю среду" для тестируемого кода, изолируем его от всех зависимостей, но имитируем всё так, как будто "внешняя среда" существует (библиотеки, базы данных, сеть). Можно даже создавать исключительные ситуации, которые сложно воспроизвести в реальности.

Итог

К счастью, многие разрабатывают модульные тесты для своих программ. Но далеко не всегда при это люди знают про mock-объекты. Или называют их просто "пустышками", упуская возможности, которые они предоставляют. Библиотеки для создания и управления mock-объектами существуют в любом языке. Например, в питоне (с версии 3.3) есть стандартный модуль unittest.mock. В java есть JUnit и расширение JMock. Даже для языка C есть библиотека cmocka.org, которая поддерживает mock-объекты. Google C++ Mocking Framework не уступает им в возможностях, но остается достаточно простым в использовании.

Целью статьи было обратить Ваше внимание на возможности применения mock-объектов в модульном тестировании. Кратко рассказать об одном из эффективных инструментов. На этом всё. Намного больше Вам расскажет русскоязычная документация:

Документация по Google C++ Testing Framework

Документация по Google C++ Mocking Framework