Юнит тесты на Си — нет ничего проще

https://habr.com/ru/post/244835/

Прочитав статью «Тестирование встроенных систем» и комментарии к ней я был несколько поражен тем фактом, что многие хабровчане знакомы с книгой «Test Driven Development for Embedded C (Pragmatic Programmers)» и framework-ом Unity, но не используют весь арсенал средств, которые предлагают ребята из throwtheswitch.org.

Хочу кратко поделится опытом использования этих самых средств.

О себе

Так получилось, что я нарабатывал свой опыт в программировании встраиваемых систем через тесты (Unit, Integration, System, Stress). За три года мне посчастливилось пройти путь от Junior'a и написания тестов, покрывающих код других специалистов, до Senior'a с опытом разработки систем с использованием TDD методологии.

Обещанное

Упомянутый выше framework Unity очень прост и удобен в использовании. Но это всего лишь вершина айсберга. На странице throwtheswitch.org есть следующие инструменты.

CMock — инструмент позволяющий автоматически генерировать Си-код mock-ов для Ваших тестов. Написан на Ruby. Утверждаю, как человек, который на протяжении трех лет «генерировал» mock-и руками — это просто подарок для Си-разработчика. Но использовать его автономно без следующего инструмента, на мой взгляд, не рационально.

Ceedling — это целая билд-система, как утверждают сами авторы. Но по сути — это все, что Вам нужно для работы. Данный пакет содержит в себе все необходимое: Unity («тест-раннеры» и «чекалки» значений), CMock (генератор моков) и поддержку командной строки через ruby make.

Other — под этим странным заголовком находится очень, полезный, на мой взгляд инструмент — CException. Невероятно маленькая библиотека для Си позволяющая получить некое подобие исключений. Но дезинформировать не буду. В проектах использовать не довелось.

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

Прежде всего, Ceedling должен быть корректно установлен и проверен на работоспособность как указано тут.

После установки создаем папку и тестовое окружение проекта командой:

ceedling new MyNewProject

В результате будет создана папка MyNewProject внутри которой будут сгенерированы следующие папки и файлы:

  • build — сюда будут помещаться все артефакты при сборке и прогоне тестов
  • src- это место для нашего «боевого» кода, который подлежит тестированию
  • test — будут лежать все наши тесты
  • vendor — собственно сам framework, с документацией и плагинами
  • project.yml — конфигурационный файл тестового проекта. Позволяет делать хороший тюнинг, но это с опытом

Пора писать первый тест.

Поместим в папку test файл test_calc.c следующего содержания:

#include "unity.h"
#include "calc.h"
void setUp(void)
{
}
void tearDown(void)
{
}
void test_add( void )
{
    int result = 0;
    result = calc_add(2,2);
    TEST_ASSERT_EQUAL_INT( 4, result );
}

Запускаем тест командой:

ceedling test:test_calc.c

Результат ожидаемый. Тест есть, кода нет. Проект не может быть собран.

Добавляем код.
В папку src помещаем два файла:

«calc.h»

#ifndef CALC_H
#define CALC_H
int calc_add(int a, int b);
#endif

«calc.c»

#include "calc.h"
int calc_add(int a, int b)
{
    return a + b;
}

Повторяем сборку и попытку прогнать тест:

ceedling test:test_calc.c

Если все сделано правильно, то в консоли должны быть результаты теста:

Test 'test_calc.c'
------------------
Compiling test_calc_runner.c...
Compiling test_calc.c...
Compiling calc.c...
Compiling unity.c...
Compiling cmock.c...
Linking test_calc.out...
Running test_calc.out...

-------------------------
OVERALL UNIT TEST SUMMARY
-------------------------
TESTED:  1
PASSED:  1
FAILED:  0
IGNORED: 0

Этот короткий пример показывает, что test-runner был сгенерирован и добавлен в сборку автоматически. Его код можно найти в папке build/test/runners.

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

«calc.c»

#include "calc.h"
#include "rules.h"
int calc_add(int a, int b)
{
    if (rules_is_addition_allowed())
    {
        return a + b;
    }
    return 0;
}

Добавим еще один файл в папку src:

«rules.h»

#ifndef RULES_H
#define RULES_H
int rules_is_addition_allowed(void);
#endif

Попытка запустить тест будет неудачной, так как нет определения для функции rules_is_addition_allowed().

Самое время воспользоваться CMock.
Изменим тест следующим образом:

#include "unity.h"
#include "calc.h"
#include "mock_rules.h"
void setUp(void)
{
}
void tearDown(void)
{
}
void test_add( void )
{
    int result = 0;
    rules_is_addition_allowed_ExpectAndReturn(1);
    result = calc_add(2,2);
    TEST_ASSERT_EQUAL_INT( 4, result );
}
void test_add_off_nominal( void )
{
    int result = 0;
    rules_is_addition_allowed_ExpectAndReturn(0);
    result = calc_add(2,2);
    TEST_ASSERT_EQUAL_INT( 0, result );
}

Таким образом, мы получили автоматически сгенерированный mock одним лишь указанием "#include «mock_rules.h». Исходный код данного файла можно найти в директории build/test/mocks. Его изучение даст хорошее представление о том, каким образом можно менять поведение подменяемого модуля.

Оговорочки

1. Я использую данный framework только для тестирования кода на PC. Это диктует определенные правила к архитектуре разрабатываемого ПО. Прогонять юнит тесты на реальном железе смысла не вижу. HAL — он либо работает либо нет и тестируется мануально (мое видение ситуации);
2. Я не использую данный framework для тестирования нескольких потоков. Потокобезопастность данного инструмента мной не исследовалась;
3. Данная статья не учит как правильно писать код и/или тесты, а всего-лишь дает краткое представление об упомянутых выше инструментах разработки.

Теги:
Хабы:
+31
11 +11
 

Редакторский дайджест

Присылаем лучшие статьи раз в месяц

 




Комментарии 11

 

 


 


А у меня напротив опыт использования Unity на реальном железе. Огромный плюс — ее портабельность и минимализм: жужжит под win/wec7/linux/vxworks и с любым компилятором.

Правда признаюсь тесты — функциональные, unit тесты не осилили, а поэтому моки и раннеры не задействованы.

 

 

Спасибо, будем иметь в виду. Такой арсенал платформ достоин внимания.

Думаю, что моки и раннеры тоже будут жить. Просто надо быть внимательным с инклудами (не включать лишнего) и не злоупотреблять printf() в чистом виде в надежде потом просто за-mock-ать <stdio.h> — framework-у тоже надо как-то печатать. Но это теория…


 



 


Неплохая статья, на сайте Unity и правда все менее понятно.
Арсенал средств это CMock? Mock хорошая штука, и ребята молодцы, что пробуют добавить их в Си. Однако версия в Unity слишком уж урезана. По сути мы можем только проверить результат возврата (например, можно легко сэмулировать что в куче закончилась память). Но ведь моки чаще применяются когда дорого использовать реальные ресурсы типа БД, либо нужно быстро разработать прототип (не ясно как это сделать CMock'ом из-за его ограниченности).

 

 

Арсенал средств это CMock?

+ CException — упомянут вскользь
+ Ceedling — позволяет мне писать только тест, не думая, как его запускать, где мой main, и как сделать сборку и запуск тестов автоматизированно на билд-сервере.
+ Ceedling Plugins — не упоминал, дабы не нагружать лишним. Цель статьи — максимально облегчить старт.

Хотя, может быть, Вы и правы, Арсенал — слово громкое.

По сути мы можем только проверить результат возврата

Это не всегда так. В моем примере функция без параметров. Но если, к примеру, у нас такой вариант:

// API that shall be mocked
int API_foo( int int_param, const char * str_param);

// tests
void test_another_foo()
{
  API_foo_ExpectAndReturn( 20, "expected string", 100 );
    
  API_target();
}

Тут мы ожидаем, что в результате вызова тестируемой функции API_target() будет вызвана за-mock-аная функция с параметрами 20 и «expected string» и в результате такого вызова mock вернет 100. Параметры тоже проверяются.
К тому же, если идет серия вызовов mock-ов, то порядок и количество вызовов учитываются.
Опять же — всего не расскажешь.

Но ведь моки чаще применяются когда дорого использовать реальные ресурсы типа БД, либо нужно быстро разработать прототип

Наверное Вы правы, но это не в целях юнит тестирования. Если я правильно понял, то Вы желаете «пощупать» как работает приложение, на ранних стадиях, когда еще далеко не все написано. Тут надо осознанно разрабатывать некий имитатор с необходим поведением. Тут framework-и не очень то помогают (говорю только в части касающейся Си и Embedded). Хотя это тоже называется mock-ом.
 


 



 


Делал презентацию для сотрудников Связка check + gcov + ggcov + make для TDD на С/Embedded Linux. Мешанина из unit-тестов и функциональных.

Плюсы подхода:

  • Сразу в удобоваримом виде получаем результаты прогона и % покрытия кода тестами
  • Очень легко найти мёртвый код
  • Система компонентная. Можно менять сборку, утилиты для просмотра покрытия, etc
  • Минус — не хипстерский, пишем тесты руками. Никаких ruby и генераторов тестов.

 

 

Спасибо за материал.
Я сам фанат этого дела, и согласен с Вами в том, что покрытие — очень важный инструмент.
Минус — не хипстерский, пишем тесты руками. Никаких ruby и генераторов тестов.

Так не в «шашечках» дело. Вы можете с успехом пользоваться gcov и на Ceedling. Есть плагин для этого.

How-To

  • в project.yml в самом низу находим и добавляем
    :plugins:
      :enabled:
        - gcov  # add this line
    
  • вместо rake test:all используем rake gcov:all
  • ищем артефакты в build/gcov (если мне не изменяет память)


 



 


Other — под этим странным заголовком находится очень, полезный, на мой взгляд инструмент — CException. Невероятно маленькая библиотека для Си позволяющая получить некое подобие исключений. Но дезинформировать не буду. В проектах использовать не довелось.

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

 

 

Если вы не использовали эту штуку, то почему вы пишете, что она очень полезная?

К сожалению, далеко не все зависит от моих предпочтений. Иногда, клиент против, иногда мое руководство, иногда коллеги.
Я бы с огромным удовольствием обкатал это дело на настоящем проекте, но пока приходится выгребать "Old surly error handling".

 

 

Простите, но я по-прежнему не понимаю. Вы ей вообще пользовались или нет?

 

 

Ответ на Ваш вопрос — нет.

 

 

Понятно, спасибо.

Добавить комментарий

Обратная связь

Интересуют вопросы реализации алгоритмов, программирования, выбора электроники и прочая информация, постараюсь осветить в отдельных статьях

пишите мне на netdm@mail.ru