Полгода назад на моем проекте было примерно около 0% покрытие кода юнит-тестами. Простых классов было достаточно мало, создавать для них юнит-тесты было легко, но это было относительно бесполезно, так как на самом деле важные алгоритмы находились в сложных классах. А сложные, с точки зрения поведения, классы было трудно юнит-тестировать так как такие классы были завязаны на другие сложные классы и классы конфигурации. Создать объект сложного класса и тем более его протестировать юнит-тестами было невозможно. Некоторое время назад я прочёл "Writing Testable Code" в Google Testing Blog . Ключевая идея в статье заключается в том, что C++ код, пригодный для юнит-тестирования, пишется совсем не так, как привычный C++ код. До этого у меня было впечатление, что для написания юнит-тестов наиболее важен фреймворк для юнит-тестирования. Но все оказалось не так. Роль фреймворка — второстепенна, прежде всего требуется писать именно код, пригодный для юнит-тестирования. Автор для этого использует термин "testable code". Или, как мне кажется более точным, "unit-testable code". Затем все достаточно просто. Для testable code можно сразу писать ЮТ и тогда будет Test Driven Development (TDD), можно и позже, код все равно это позволяет. Я пишу тесты сразу с кодом, а потом смотрю по coverage report забытые и пропущенные места в коде и дополняю тесты. В своей статье автор приводит несколько принципов. Я отмечу и прокомментирую самые важные, с моей точки зрения. Абсолютно важный принцип. Фактически любой сложный класс обычно создает несколько классов других объектов внутри себя. Например в конструкторе или в ходе обработки конфигурации.  Обычный подход — использовать new прямо в коде класса. Это совершенно неправильно для юнит-тестирования. Если так создавать класс, то в итоге получится именно куча слипшихся объектов классов, которые невозможно протестировать. Правильный подход с точки зрения ЮТ — если классу требуется создавать объекты, то класс должен получать на вход указатель или ссылку на интерфейс класса-фабрики.  Пример: Я обычно возвращаю именно std::shared_ptr из методов класса-фабрики. Таким образом непосредственно в юнит-тестах можно сохранять
созданные тестовые объекты и проверять их состояние. Еще. В фабрике я не только создаю объекты, но и могу делать отложенную инициализицию объектов. Объекты с которыми взаимодействует класс должны ему предоставляться непосредственно. Например вместо того, чтобы передавать классу ссылку на объект класса application, у которого конструктор класса получит ссылку на объект meta::class_repository, стоит передавать в конструктор класса ссылку на meta::class_repository. При таком подходе в юнит-тестах достаточно создать объект meta::class_repository, а не создавать объект класса application. Тут важная мысль у автора:  Интерфейсы важны. Нет интефейсов — нет возможности тестировать. Пример.
Мне требовалось написать юнит-тесты для failover сервиса. Он завязан на библиотечный класс zookeeper::config_service в своей работе. "Швов" не было у zookeeper::config_service. Попросил разработчика zookeeper::config_service добавить интерфейс zookeeper::config_service_i и добавить наследование zookeeper::config_service от zookeeper::config_service_i. Если бы не было возможности добавить интерфейс так просто, то использовал бы прокси объект и интерфейс для прокси-объекта. Наследование склеивает классы и делает сложным юнит-тестирование отдельного класса. Так что лучше без наследования.  Однако иногда без наследования не обойтись. Например: Это пример, когда метод on_message требуется определять в дочернем классе и без наследования от класса AMQP::service_interface не обойтись. В таком случае я не добавляю сложные алгоритмы в amqp_service::on_message(). В вызове amqp_service::on_message() я делаю сразу вызов input_handlers::add_message(). Таким образом логика работы по обработке AMQP сообщения переносится в input_handlers, который уже написан правильно с точки зрения юнит-тестирования и который я могу полностью протестировать. Важная идея. Классы сервисных объектов сложны и их объекты создаются в фабриках. С точки зрения трудозатрат одновременная разработка кода и юнит-тестов заметно увеличивает время разработки. Вот примерно такие есть варианты: 1) Если просто покрывать основные сценарии.
2) Если дополнительно покрывать "dark corners", которые видны только по coverage отчету и которые обычно тестировщик просто может не проверять и, как следствие, не тратить на это время.
3) Если добавлять юнит-тесты для негативных, редких или сложных сценариев. Например, ЮТ для проверки изменения количества воркеров в конфигурации на ходу при пустой и непустой очереди на обработку.
4) Если код был не testable, а задача доработать с добавление фичи и юнит-тестов, что потребует рефакторинг. Не буду давать точных оценок, но мое впечатление, что если юнит-тестирование выполнять не только для основного сценария, а с учетом пунктов 2 и 3, то время разработки вырастает на 100% по сравнению просто с разработкой без юнит-тестов. Если же код не testable, а в него добавляется фича с юнит-тестами, то рефакторинг такого кода для того, чтобы превратить его в testable увеличивает трудозатраты на 200%. Дополнительный нюанс по трудозатратам. Если разработчик подходит к написанию ЮТ тщательно и делает все из пунктов 1, 2 и 3, а тимлид считает, что юнит-тесты — это в основном пункт 1, то возможны вопросы,
почему так долго ведется разработка. Еще есть вопрос по производительности такого testable кода. Один раз я слышал такое мнение, что наследование от интерфейсов и использование виртуальных функций влияет на производительность и поэтому так писать код не стоит. И как раз удачно одна из задач у меня была увеличить производительность обработки AMQP сообщений в 5 раз до 25000 записей в секунду. После выполнения этой задачи я сделал профилирование на Linux работы программы. В топе были pthread_mutex_lock и pthread_mutex_unlock, которые шли из аллокаторов классов. Накладные расходы на вызовы виртуальных функций просто не оказали какого-то заметного влияния. Вывод по производительности у меня получился такой, что использование интерфейсов не оказало влияния на производительность. В заключение, вот оценки покрытия тестами для некоторых файлов на моем проекте после перехода на разработку с юнит-тестами. Файлы failover_service.cpp, input_handlers.cpp и input_handler.cpp были разработаны именно с использованием "Writing Testable Code" и имеют высокую степень покрытия кода юнит-тестами. Дополнение Построение отчета я делаю так: Дополнение №2 Для юнит-тестирования алгоритмов, которые должны выполнить определенное действие, например каждую минуту или каждый час, я таким алгоритмам передаю одним из параметров функцию получения времени: А в юнит тесте используется уже другая функция получения времени. Например вот функция времени, которая позволяет перейти на следующую минуту выполнением ++minute_passed: