Споры о применении модульного тестирования в разработке встраиваемого ПО не утихают, масла в пожар этих споров подливают статьи, иногда появляющиеся на Хабре, такие как Модульное тестирование в Embedded или очередное упоминание не безызвестной и, несомненно хорошей, книги "Test-Driven Development for Embedded C" авторства James W. Grenning. В целом с методологией TDD можно спорить, как и любой инструмент его однозначно стоит применять там, где он уместен. Но вряд ли кто-то будет спорить с тем, что часто во встраиваемом ПО присутствуют модули бизнес-логики или математических вычислений, которые должны подвергаться тестам при рефакторинге или оптимизации и тут уже не важно используете вы TDD целиком или только берете оттуда те принципы, которые лично вы считаете полезными. Да, такие модули можно оттестировать на хост-платформе, но не стоит забывать о возможных отличиях в архитектуре, особенностях оптимизации и, наконец, потенциальных ошибках в компиляторах. Поэтому зачастую возникает необходимость иметь возможность выполнять тестирование именно на целевой платформе и с использованием единого целевого IDE и компилятора.

К сожалению, на сайте разработчиков решения для тестирования на C - Unity нет инструкции по интеграции с IAR Embedded Workbench. Поэтому давайте попробуем восполнить этот пробел и сделать статью полезной для тех, кто наконец созрел для начала использования модульного тестирования в своих проектах на IAR.

Начать можно с любого удобного для Вас проекта, под ту плату, что есть у Вас под рукой. В моем случае это P-NUCLEO-WB55 под которую был собран минималистичный проект для мигания светодиодом.

Первое что необходимо сделать для интеграции, это, собственно, скачать файлы с исходным кодом Unity, добавить группу "Unity" в проект и добавить в неё файлы Unity.

Хорошим решением будет включение в проект файлов unity_fixture*.*, это позволит создавать тесты в манере сходной с CppUTest и в дальнейшем их использование сделает написание тестов удобнее и проще. Стоит ли упоминать, что эти расширения изначально были предложены тем самым James Grenning, автором вышеупомянутой книги и они широко используются в его примерах.

Добавьте в "include directories" путь к папке с файлами Unity

Так как будет полезно иметь возможность запуска тестов как в хост-системе, так и на целевой платформе, сделаем две отдельных конфигурации. Просто сделайте копии основной конфигурации Вашего проекта.

Определим символы UNITY_FIXTURE_NO_EXTRAS и UNITY_TEST для конфигурации Test_Hardware

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

В конфигурации Test_Host, которая будет использоваться для запуска на хост-платформе, установите stdout через semihosting, чтобы иметь возможность видеть отладочный вывод в окне терминала

Также в этой конфигурации укажите "Simulator" в качестве отладчика

Мы определили (или не определили, в зависимости от конфигурации) три символа:

UNITY_FIXTURE_NO_EXTRAS - предписывает Unity не включать расширения, в первую очередь "Unity Memory" для слежения за кучей. Я, как приверженец MISRA C, стараюсь максимально избегать использования кучи в своих проектах, если у Вас иная точка зрения Вы можете не включать это определение и добавить необходимые файлы из Unity.

UNITY_TEST - определен в конфигурациях для запуска тестов.

UNITY_TEST_HOST - определен в конфигурации для запуска тестов на хост-системе.

Конфигурации готовы, можно преступить к интеграции Unity и реализации тестов. Откройте файл с функцией main и добавьте в начало директиву условной компиляции с подключением заголовочных файлов Unity для тестовых конфигураций

#if defined(UNITY_TEST)
  #include "unity.h"
  #include "unity_fixture.h"

  void RunAllTests(void);
#endif

В самой функции main спрячьте инициализацию периферии в условную компиляцию для конфигурации тестов на хост-системе

#if !defined(UNITY_TEST_HOST)  
  SystemClock_Config();
  Configure_GPIO();
#endif

И замените запуск основного цикла (или того, что выполняет у Вас его роль) запуском теста для тестовых конфигураций

#if !defined(UNITY_TEST)  
  while (1)
  {
    LL_GPIO_TogglePin(LED2_GPIO_PORT, LED2_GPIO_PIN);
    LL_mDelay(150);
  }
#else
  UnityMain(0, NULL, RunAllTests);
  while(1);
#endif

Осталось добавить сами тесты. Описание разработки тестов выходит за пределы данной статьи, существует много неплохой литературы на эту тему, например, упомянутая выше книга "Test-Driven Development for Embedded C". К сожалению, её автор запретил использование своих примеров в сторонних статьях и обучающих материалах, поэтому напишем свой простой тест для функции sqrtf.

Создадим группу Tests и добавим туда файлы AllTests.c и Sqrt.c

AllTests.c
#include "unity_fixture.h"

//------------------------------------------------------------------------------
void RunAllTests(void)
{
    RUN_TEST_GROUP(sqrt);
}

//------------------------------------------------------------------------------
TEST_GROUP_RUNNER(sqrt)
{
    RUN_TEST_CASE(sqrt, Positive);
    RUN_TEST_CASE(sqrt, Zero);
    RUN_TEST_CASE(sqrt, Negative);
}

Sqrt.c
#include "unity_fixture.h"
#include <stdio.h>
#include <math.h>

//------------------------------------------------------------------------------
TEST_GROUP(sqrt);

//------------------------------------------------------------------------------
TEST_SETUP(sqrt)
{
}

//------------------------------------------------------------------------------
TEST_TEAR_DOWN(sqrt)
{
}

//------------------------------------------------------------------------------
TEST(sqrt, Positive)
{
    TEST_ASSERT_EQUAL_FLOAT(2.0, sqrtf(4.0));
}

//------------------------------------------------------------------------------
TEST(sqrt, Zero)
{
    TEST_ASSERT_EQUAL_FLOAT(0.0, sqrtf(0.0));
}

//------------------------------------------------------------------------------
TEST(sqrt, Negative)
{
    TEST_ASSERT_FLOAT_IS_NAN(sqrtf(-4.0));
}

Проект готов за исключением одной маленькой детали - будет полезно исключить из конфигурации, не предполагающей тестирование, группы Unity и Tests. Переключитесь на основную конфигурацию и щелкните правой кнопкой мыши на группе, затем выберете в выпадающем меню пункт "Options" и установите чек бокс "Exclude from build"

Выбранные группы станут серыми и не будут использоваться в конфигурации.

Можно приступить к тестированию. Перейдите на конфигурацию Test_Host и запустите отладку

после этого перейдите в меню "View" и сделайте видимым окно "Terminal I/O", теперь нажимайте кнопку "Go".

Если все сделано верно в окне Terminal I/O вы увидите результат выполнения тестов на хост-платформе

Выполнив аналогичные действия (запуск отладки) в конфигурации Test_Hardware вы сможете запустить тесты непосредственно на целевой платформе.

Теперь у Вас есть возможность для запуска модульных тестов как на хост-платформе, так и на целевом железе из одной среды разработки с использованием одного и того же компилятора. Самое время освоить написание тестов и покрыть ими модули своего кода.

Но осталась еще одна деталь, на которой хотелось бы остановиться. Запуск модульных тестов крайне полезен при рефакторинге и оптимизации кода, чтобы убедиться в том, что новые изменения не нарушили какой-либо функциональности и вы непреднамеренно не внесли какую-либо ошибку. Также в процессе оптимизации было бы очень полезно иметь какую-то метрику для оценки времени выполнения участков кода на целевой платформе. Если вы используете микроконтроллер с ARM ядром в составе которого реализован Data Watchpoint and Trace (DWT) unit, то вы можете воспользоваться регистром CYCCNT модуля DWT для подсчета количества тактов процессора потраченных на выполнение участка кода.

Сделать это можно следующим образом.

Добавьте в файл с функцией main функции для инициализации модуля DWT и получения счетчика тактов

//------------------------------------------------------------------------------
inline void DWT_Init(void)
{
	CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
	DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
	DWT->CYCCNT = 0U;
}

//------------------------------------------------------------------------------
inline uint32_t DWT_GetCounter(void)
{
	return DWT->CYCCNT;
}

Модифицируйте место с запуском теста следующим образом

#else
  __disable_interrupt();
  DWT_Init();
  UnityMain(0, NULL, RunAllTests);
  printf ("Execution time %d ticks\n", DWT_GetCounter());
  while(1);
#endif

При запуске на целевой платформе вы получите вот такой вывод с оценкой времени выполнения тестов в тактах процессора

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

Проект целиком можно взять на github.

Успешного всем тестирования и поменьше ошибок и проблем в ваших проектах.

Комментарии (4)


  1. Indemsys
    28.11.2022 12:00

    Когда пишут про тестирование самое интересное по какой технологической схеме идёт разработка. Какого объёма софт, сколько человек его пишет, про что этот софт, длительность цикла разработки.

    Потому что показывать преимуществ подхода с юнити на примере тестирования sqrt это как показывать преимущества C++ перед C на примере вывода hello word. Т.е. одно недоразумение. Да к тому же тут на хабре были статьи что, типы float нельзя проверять на совпадение.

    Я бы добавил ещё во встраиваемых системах критически важно в какой памяти и каким образом выделенной код работает. Даже выравнивание по границам блоков имеет значение.
    Скажем тестирование функций с float может показать в отдельной фикстуре юнити все нормально, а помещённая в задачу RTOS будет сбоить. Просто потому что стек задачи неправильно выровнен.


    1. Whitech Автор
      28.11.2022 12:10
      -1

      Статья не о тестировании как таковом, о том как начать его использовать в конкретной IDE, на примере конкретного Unity. Она предназначается для тех, кто уже созрел использовать модульное тестирование, но не знает с чего начать. Собственно, и название статьи соответствующее. Описание методологий тестирования находится за пределами моих намерений и на эту тему существует масса полезной литературы.

      Да к тому же тут на хабре были статьи что, типы float нельзя проверять на совпадение.

      А никто их и не пытается проверять на совпадение. С чего Вы это взяли? Если Вам интересно, разверните макрос TEST_ASSERT_EQUAL_FLOAT и убедитесь, что там делается именно то, о чем сказано уже много раз.


      1. Indemsys
        28.11.2022 12:43
        -1

        Ну тогда объясните какую величину вы присвоили макросу UNITY_FLOAT_PRECISION и на основании чего. И как докажете кроссплатформенность такого решения.

        И даже если вы просто демонстрируете способ выполнения чего-то то не помешало бы все же сказать ЗАЧЕМ? Зачем так проверять функцию sqrt? Почему вы не проверяете весь ряд действительных чисел на этой функции?


        1. Whitech Автор
          28.11.2022 13:07
          +1

          Ну тогда объясните какую величину вы присвоили макросу UNITY_FLOAT_PRECISION и на основании чего. И как докажете кроссплатформенность такого решения.

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