Как стать автором
Обновить

Интеграция Unity Framework для модульного тестирования в IAR Embedded Workbench

Время на прочтение6 мин
Количество просмотров1.6K

Споры о применении модульного тестирования в разработке встраиваемого ПО не утихают, масла в пожар этих споров подливают статьи, иногда появляющиеся на Хабре, такие как Модульное тестирование в 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.

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

Теги:
Хабы:
Всего голосов 6: ↑6 и ↓0+6
Комментарии4

Публикации

Истории

Работа

Ближайшие события