Меняем фреймворк юнит-тестирования одной строчкой кода
Как сменить фреймворк для юнит-тестирования на С и С++, если количество самих тестов слишком велико, а писать их заново не хочется.
1К открытий2К показов
Коротко, о чем статья: использование различных фреймворков юнит-тестирования для поддержки качества кода прошивок микроконтроллеров на языке С/С++, способ миграции на новый фреймворк, если на старом уже написано большое количество тестов.
Кирилл
Разработчик Новео
Введение
В современной разработке сложно представить себе серьезный проект, в котором не использовались бы практики CI/CD, а код не был бы покрыт юнит-тестами, однако при создании прошивок для микроконтроллеров далеко не каждая команда уделяет время настройке и поддержке этих процессов. При работе над одним из медицинских приборов мы обнаружили, что имеющиеся юнит-тесты покрывают только те части кода, которые отвечают за логику и программные вычисления, но совсем не затрагивают тестирование аппаратных функций. Эта статья про то, почему нужно обязательно тестировать вызовы к HW, почему пришлось менять фреймворк юнит-тестирования и как удалось упростить процесс перехода.
Зачем тестировать аппаратные функции?
Ответ на этот вопрос очень простой – затем же, зачем и неаппаратные, то есть, чтобы быть уверенным, что код работает именно так, как ожидается. Некоторые разработчики попадают в ловушку, предполагая, что если используются документированные SDK API, то можно целиком на них положиться и не перепроверять их работу. Однако представим себе разработку устройства на стадии подготовки прототипа. В любой момент может быть принято решение об изменении версии SDK или микроконтроллера, и в этом случае точно придется перепроверять работу аппаратных функций. Такие миграции возможны и при разработке очередной версии уже готового устройства, когда кодовая база успела значительно разрастись. А если продукт уже вышел в серийное производство, то понадобится проверка каждого устройства в заводских условиях. Для этого пишутся специальные тестовые прошивки, базой для которых легко может стать набор юнит-тестов для аппаратных функций.
Еще одна потенциальная проблема – запуск юнит-тестов на ПК недостаточен для проверки правильности вычислений. Из-за различий в аппаратной архитектуре самые обычные операции, например, арифметика чисел с плавающей точкой, могут давать разные результаты.
Ориентируясь на эти тезисы, мы занялись исправлением ситуации на проекте.
Выбираем фреймворк
На проекте на тот момент уже использовался популярный фреймворк GoogleTest для юнит-тестирования кода на С++. О его преимуществах говорить не будем, их много, и статья не о них. В нашем случае были важнее его недостатки, а именно – размер собранной библиотеки, который «съедал» солидный кусок из наших скромных 256KB.
Можно было, конечно, пропатчить код, оставив только используемые куски и сократив, таким образом, размер библиотеки, но это скорее походило на то, что мы используем неподходящий инструмент, чем на хорошее решение. Тем более, что был найден другой, гораздо более простой и легковесный фреймворк Unity (нет, никакого отношения к геймдеву не имеет), состоящий из пары заголовочных файлов и одного С-файла (что будет преимуществом, если проект написан на чистом С). Так как каких-то сложных тестовых сценариев нам писать не требовалось, то для тестирования аппаратных функций мы решили выбрать именно его.
На этой стадии получалось, что мы используем Unity для SDK API, и GoogleTest для всего остального. Вариант, в целом, рабочий, и в какой-то момент мы даже думали остановиться на нем несмотря на то, что иметь в проекте разные фреймворки для, по сути, одной задачи, это явно перебор. Но переводить уже имеющуюся базу тестов на новый фреймворк никто не горел желанием. Однако вскоре этот вопрос поднялся снова – часть тестов была направлена на проверку корректности вычислений, и требовала дублирования тестов на обоих фреймворках. А это уже было неприемлемо. Пришлось лезть внутрь исходников GoogleTest и смотреть на реализацию используемых тестовых макросов. Довольно быстро стало ясно, что можно написать собственный преобразователь GoogleTest => Unity, который позволит оставить код имеющихся тестов неизменным.
Пишем gtest2unity.h
В целом, идея довольно простая – переопределить макросы старого фреймворка собственными, которые используют макросы нового. В этом случае будет достаточно подменить заголовочный файл, не меняя ни строчки кода в тестах.
Начинаем с простого: устанавливаем соответствия между макросами проверки:
Что кроется за этими макросами нам не очень интересно, т.к. передаваемые параметры совпадают, и по имени можно определить, что проверки выполняются одинаковые. Оба фреймворка обладают внушительным арсеналом различных проверок, и вполне вероятно, что некоторым не найдется аналога, но для обычных юнит-тестов такой проблемы не возникает.
Теперь переходим к чуть более сложной части. Что еще используется в имеющихся тестах, связанное с GoogleTest?
Что ж, тут тоже очевидно, что нужен собственный класс testing::Test, от которого мы сможем наследоваться подобным образом.
Теперь переходим к последней части, зависимой от GoogleTest:
Здесь уже приходится обращаться к исходному коду GoogleTest, потому что догадаться, что скрыто за макросом TEST_F, довольно сложно (заинтересованным рекомендую обратиться к гитхабу проекта и изучить макрос GTEST_TEST_ в файле gtest-internal.h). Оказалось, что за TEST_F спрятано довольно много кода, большая часть из которого нам не нужна. Главные пункты, которые нам пришлось адаптировать:
– каждый тест-кейс – отдельный класс, унаследованный от нашего PumpDriverTests;
– тело тест-кейса – реализация приватной функции этого класса;
– нужна функция, которая создаст экземпляр этого класса, а затем последовательно вызовет его методы.
Сначала определим пару макросов для удобства, чтобы легко получать уникальные имена для наших классов и функций:
А затем реализуем описанные пункты:
Вот мы и получили рабочий вариант gtest2unity.h. Теперь для перевода файла с GoogleTest на Unity достаточно простого:
Ну и, конечно, добавить файлы Unity в систему сборки (например, в CMakeLists.txt). Ошибок компиляции больше нет, и наша работа закончена… или нет? Скомпилировать код без ошибок — это, конечно, хорошо, но сами тесты еще и запустить надо. GoogleTest определяет необходимые методы по умолчанию, но в Unity нужно все делать руками. Программисты, как известно, не очень любят ручную работу, поэтому мы решили написать простой генератор кода запуска тестов.
Генерируем test_runner.cpp
Запустить тесты с помощью Unity не сложно:
Над генератором долго не думаем – выбираем Python и вперед! Хардкодим подключение заголовочных файлов и части с UNITY_BEGIN и UNITY_END в локальные строки. Дальше парсим исходники, но и тут все довольно тривиально – находим макрос TEST_F в коде, вытаскиваем его параметры и записываем их в макрос EXTERN_TEST_F_NAME дважды – первый раз перед main() вместе с extern объявлением (даем понять, что сама функция находится в другом файле), второй уже внутри RUN_TEST, чтобы, соответственно, запустить сам тест.
Собрав информацию обо всех тест-кейсах, генерируем итоговый файл (который также нужно включить в систему сборки). Теперь и компиляция, и линковка, и запуск – все готово к тому, чтобы наши тесты падали при неудачных изменениях кода.
Заключение
В заключение хочется сказать, что мы ни в коем случае не рекомендуем использовать подобные трюки без крайней необходимости. В нашем случае достаточно было принять во внимание необходимость тестировать SDK API и с самого начала выбрать подходящий фреймворк юнит-тестирования. Однако мы редко встречаемся с идеальными условиями, поэтому стоит иметь ввиду, что иногда можно исправить ситуацию и с таким подходом.
Несомненно, приведенный код далек от идеала (а тот, который не показан, тем более), но для нашей ситуации его оказалось достаточно, и мы смогли продолжить работу над проектом, теперь уже покрывая тестами весь код.
1К открытий2К показов