TDD для микроконтроллеров. Часть 1: Первый полет

    TDD для микроконтроллеров. Часть 1: Первый полет
    TDD для микроконтроллеров. Часть 2: Как шпионы избавляют от зависимостей
    TDD для микроконтроллеров. Часть 3: Запуск на железе


    Встраиваемые системы широко применяются в бытовой электронике, промышленной автоматике, транспортной инфраструктуре, телекоммуникациях, медицинском оборудовании, а также в военной, аэрокосмической технике и т. д. Хотя последствия любой ошибки проектирования обходятся дорого, ошибку в ПО для ПК или в большом корпоративном приложении обычно относительно легко исправить. А если дефект будет во встраиваемом ПО (далее – ВПО) электронного блока управления тормозной системой автомобиля, то это может вызвать массовый и дорогостоящий отзыв продукции.


    Сфера применения встраиваемых систем постоянно расширяется, сложность выполняемых ими задач растет. Это в свою очередь повышает риск внесения ошибок в процессе разработки, что увеличивает вероятность весьма дорогостоящих дефектов в ПО.


    Одной из наиболее популярных методологий улучшения качества разрабатываемых приложений является Test-driven development (TDD). Но эффективна ли методология TDD для разработки встраиваемых систем? Ответ на этот вопрос будем искать под катом.


    Эффективность TDD


    Все большее число разработчиков придерживаются мнения, что методология TDD имеет ряд преимуществ над Test-last development (TLD). При этом TDD понимается как процесс итеративного, непрерывного написания тестов и рабочего кода с обязательными фазами рефакторинга.



    Схема итеративного процесса разработки TDD


    В результате применения TDD можно выделить следующие улучшения при проектировании приложений:


    • TDD позволяет сконцентрироваться на дизайне и понять, как сделать его лучше;
    • TDD избавляет разработчиков от страха внесения изменений в код: при внесении некорректных изменений будет легко «отловить» ошибку с помощью запуска тестов;
    • хорошо спроектированные тесты выступают в роли показательного примера использования программного модуля, что заменяет документирование кода;
    • TDD улучшает покрытие кода тестами;
    • завершенные тесты демонстрируют прогресс разработки: каждый реализованный тест-кейс указывает на то, что очередной функционал завершен и работает;
    • уменьшается количество багов.

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


    Однако среды разработки для создания ВПО микроконтроллеров (далее – МК) не так стремительно развиваются, в некоторых из них отсутствует возможность создавать и запускать тесты, а также быстро получать результат их выполнения. К тому же один и тот же разработчик может иметь дело с разными аппаратными платформами, т. е. вести разработку с помощью разных интегрированных сред разработки (Integrated Development Environment – IDE). Поэтому возникают следующие вопросы:


    Как запускать тесты для embedded, будь то TLD или TDD?

    Будем ли мы наблюдать указанные выше улучшения в результате применения TDD при разработке встраиваемых систем?

    На эти вопросы мы попытаемся ответить в данной статье.


    Особенности разработки в embedded


    При проектировании встраиваемых систем необходимо учитывать специфику разработки ВПО:


    • ВПО запускается на МК, у которых могут быть ограниченный объем памяти, своя архитектура и т. д.;
    • ВПО выполняется в среде со специфичной аппаратной поддержкой, т. е. имеет множество библиотек для взаимодействия с различными аппаратными модулями.

    Как правило, для проектирования ВПО используется специализированная IDE. Обычно разработчик может загрузить с сайта производителя библиотеки для работы с аппаратными модулями конкретного МК – Hardware Abstraction Level (HAL). Но далеко не каждая IDE предоставляет инструменты для написания тестов ВПО. Кроме того, использование библиотек или написание собственных драйверов для взаимодействия с периферией МК вносит аппаратные зависимости в разрабатываемый код прошивки. Такой код будет работать только на конкретном МК (или на определенной серии МК).


    Так, для применения TDD при проектировании встраиваемых систем нужно ответить на ряд вопросов:


    1. Каким образом писать и запускать тесты?
    2. Что делать с аппаратными зависимостями?
    3. Как организовать непрерывную и итеративную разработку?

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


    Планируемый цикл статей


    1. В первой части (вы сейчас ее читаете) мы определим цель и инструменты разработки, затем напишем простейший тест, запустим его и представим результат.
    2. Во второй части рассмотрим процесс разработки ВПО по методологии TDD, реализуем основную платформонезависимую логику нашего проекта и применим методы для разрешения аппаратных зависимостей с целью тестирования нашего кода.
    3. В третьей части допишем платформозависимый код (драйвер) и запустим ВПО на МК STM32F103C8, подведем итоги, учитывая материалы всего цикла статей, и перечислим плюсы и минусы применения TDD при разработке ВПО для МК.

    Все исходники проекта выложены на GitLab.


    Разрабатываемый функционал


    Многие embedded-устройства могут подключаться к ПК для конфигурирования каких-либо параметров, т. е. в таком устройстве содержатся настройки, которые можно считывать или записывать. Мы приведем пример реализации именно такого функционала. Для подключения к ПК будем использовать UART-интерфейс, а в качестве энергонезависимой памяти – флеш-память МК. Таким образом, нам необходимо реализовать следующий функционал:


    • подключение устройства к ПК по UART-интерфейсу;
    • сохранение параметров в энергонезависимой памяти с помощью ПК по UART-интерфейсу;
    • считывание параметров из энергонезависимой памяти с помощью ПК по UART-интерфейсу.

    Выбор аппаратной платформы


    Для реализации нашего проекта мы выбрали отладочную плату с МК STM32F103C8, потому что МК STM32 одни из самых популярных в настоящее время, а отладочная плата стоит недорого и ее легко приобрести.



    В качестве энергонезависимой памяти в выбранном МК может быть использована флеш-память. Однако следует помнить о том, что код ВПО также хранится во флеш-памяти, которая разделена на страницы. Количество и размер страниц варьируется в зависимости от линейки МК (подробно описано в Programming manual).
    Перед записью во флеш-память необходимо убедиться, что страница была предварительно стерта.


    Инструменты разработки


    Для создания тестов и основной логики проекта мы выбирали IDE на свой вкус и цвет, потому что в первую очередь разрабатывали платформонезависимый код, который можно скомпилировать и запустить на локальном ПК. Для разработки ВПО чаще всего используется либо «чистый» C, либо С++, поэтому для написания тестов ВПО нужно использовать соответствующий фреймворк для тестирования. В результате мы выбрали следующие инструменты для написания тестов и платформонезависимой бизнес-логики:


    1. В качестве IDE – Visual Studio, потому что нам нравится ее внешний вид, удобство отладки и рефакторинга кода. Данная IDE также подходит для написания кода на «чистом» C.
    2. CppUTest – простой в настройке и в освоении фреймворк для модульного тестирования, который может использоваться для написания любых unit-тестов на C/C++.

    Создание и настройка проекта в Visual Studio


    С целью написания тестов и кода нашей бизнес-логики в первую очередь мы создали новое решение в Visual Studio, добавили в него первый проект на Visual C++ с именем проекта Tests и типом «консольное приложение Windows». В этом проекте содержатся только код тестов и дополнительные программные модули для тестирования (например, spies, mocks, stubs и т. д.).


    Настраиваем проект Tests
    1. Заходим в Properties -> C/C++ -> General -> Additional Include Directories и добавляем строки:
      — $(CPP_U_TEST)\include
      — $(SolutionDir)..\Firmware\Project\Include (путь к заголовочным файлам тестируемого кода)

    2. Заходим в Properties -> Linker -> Input и добавляем строки:
      — $(CPP_U_TEST)\lib\cpputestd.lib
      — $(SolutionDir)Debug\ProductionCodeLib.lib

      Где $(CPP_U_TEST) – переменная среды Windows, в которой содержится путь к папке cpputest (см. скриншот).

      Добавляем в проект файл Tests.cpp с содержанием:

    #include "CppUTest/CommandLineTestRunner.h"
    int main(int argc, char** argv)
    {
        return RUN_ALL_TESTS(argc, argv);
    }


    Далее в этом же решении создали второй проект с именем ProductionCodeLib, тип – статическая библиотека Visual C++. В этот проект мы будем добавлять код бизнес-логики, который планируем запустить на «железе», т. е. код, компилируемый в файл прошивки для STM32F103C8.


    Настраиваем проект ProductionCodeLib

    Добавляем пути к заголовочным файлам, используемым для создания ВПО:


    • заходим в Properties -> C/C++ -> General -> Additional Include Directories и добавляем строку \$(SolutionDir)..\Firmware\Project\Include\

    После настройки впервые запустили проект, нажав на кнопку «Run», и увидели отчет о том, что ни одного теста не было выполнено:


    OK (0 tests, 0 ran, 0 checks, 0 ignored, 0 filtered out, 0 ms)


    На этом настройка завершилась, можно приступить к итеративной разработке.


    Разработка ВПО по методологии TDD


    Мы решили использовать «чистый» C, при этом старались сохранить применение базовых принципов ООП. Такой подход обычно называют псевдо-ООП, потому что «чистый» С не поддерживает классы. В соответствии с целью нашего проекта мы создали класс Configurator, в котором реализовали следующую логику:


    • обработка команд ПК по UART-интерфейсу;
    • чтение/запись во флеш-память;
    • стирание страницы флеш-памяти.

    Конечно, в первую очередь мы создавали список тестов для будущего класса. Для этого брали блокнот и ручку (клавиатуру и текстовый редактор) и описывали простыми словами, какая логика нам была нужна. Такой процесс для нашего модуля занял около 5 минут. Ниже приведен тест-лист для класса Configurator.


    Тест-лист


    1. При получении команды read возвращаются данные, размещенные по указанному адресу на флеш-памяти.
    2. При получении команды write производится запись данных по указанному адресу во флеш-память.
    3. При получении команды erase производится стирание страницы с указанным номером.
    4. При получении команды help выводится список поддерживаемых команд.
    5. При получении неизвестной команды возвращается сообщение об ошибке.


    Для простоты и наглядности мы решили использовать строковый формат команд в кодировке ASCII.


    Основы CppUTest и первый тест


    Для реализации тестов создали файл ConfiguratorTests.cpp в проекте Tests, который затем постепенно наполняли новыми тестами в соответствии с методологией TDD.


    Для написания тестов с помощью CppUTest используется простая структура.


    Структура написания тестов для CppUTest:


    TEST_GROUP(TestGroupName)
    {
        void setup()
        {
        }
    
        void teardown()
        {
        }
    };
    
    TEST(TestGroupName, TestName)
    {
    }

    Где:


    • TEST_GROUP – блок кода, в котором могут содержаться методы setup() и teardown(), а также другие вспомогательные методы или переменные;
    • setup() – функция, вызываемая перед запуском каждого теста;
    • teardown() – функция, вызываемая в завершении каждого теста;
    • TEST – блок кода, в котором реализуется один тест, таких тестов может быть много;
    • TestGroupName – название группы тестов обычно совпадает с именем класса, для которого предназначены тесты;
    • TestName – название теста.

    Каждый тест должен работать независимо от любых других тестов. Поэтому перед каждым запуском теста следует создавать объект, а в завершение теста удалять его. Так, для нашего класса Configurator в простейшем случае в setup() создается экземпляр, а в teardown() удаляется. Чтобы убедиться в том, что объект успешно создается, мы добавили простейший тест для проверки значения указателя. Если объект по каким-то причинам не был создан, то указатель будет равен значению NULL. Назвали тест ShouldNotBeNull.


    Реализация теста ShouldNotBeNull:


    // ConfiguratorTests.cpp
    TEST_GROUP(Configurator)
    {
        Configurator * configurator = NULL;
        void setup()
        {
            configurator = Configurator_Create();
        }
        void teardown()
        {
            Configurator_Destroy(configurator);
        }
    };
    
    TEST(Configurator, ShouldNotBeNull)
    {
        CHECK_TRUE(configurator);
    }

    Первый тест был готов, но еще возвращал ошибки компиляции, потому что на данном этапе не были реализованы методы Configurator_Create и Configurator_Destroy. Для успешного завершения теста оставалось написать лишь эти два метода. И только на этом шаге мы написали первые строчки с реализацией функционала ВПО в проекте ProductionCodeLib. Для этого создали заголовочный файл Configurator.h и файл Configurator.c, в котором содержится реализация бизнес-логики. В Configurator.h добавили прототипы двух перечисленных методов. А в файл Configurator.c сначала добавили заглушки, т. е. оставили тело каждого метода пустым. Это было нужно для того, чтобы скомпилировать проект и запустить тесты.


    Реализация заглушек для теста ShouldNotBeNull:


    // Configurator.h
    typedef struct ConfiguratorStruct Configurator;
    Configurator * Configurator_Create(void);
    
    void Configurator_Destroy(Configurator * self);
    
    // Configurator.c
    #include "Configurator.h"
    
    typedef struct ConfiguratorStruct
    {
        char command[32];
    } ConfiguratorStruct;
    
    Configurator * Configurator_Create(void)
    {
        return NULL;
    }
    
    void Configurator_Destroy(Configurator * self)
    {
    }

    В соответствии с методологией TDD следует убедиться, что тест запускается, но завершается с ошибкой (т. к. тело метода Configurator_Create на данный момент было пустым). Пробуем запустить и получаем статус выполнения теста failed, что и следовало ожидать. Это означает, что мы успешно выполнили фазу test-fails.


    Вывод ошибки на экран при запуске теста:


    d:\\exampletdd\\tests\\tests\\configuratortests.cpp(27): error: Failure in TEST(Configurator, ShouldNotBeNull)
    CHECK_TRUE(configurator) failed
    .
    Errors (1 failures, 1 tests, 1 ran, 1 checks, 0 ignored, 0 filtered out, 2 ms)

    Для перехода на следующую фазу test-passes необходимо было заполнить тело конструктора. Мы добавили в метод Configurator_Create выделение памяти и возврат указателя на объект, этого достаточно для успешного выполнения теста ShouldNotBeNull. Также следовало освободить выделенную память в завершение теста, поэтому заполнили тело деструктора Configurator_Destroy.


    В итоге Configurator_Create и Configurator_Destroy выглядят так:


    Configurator * Configurator_Create(void)
    {
        Configurator * self = (Configurator*)calloc(1, sizeof(ConfiguratorStruct));
        return self;
    }
    
    void Configurator_Destroy(Configurator * self)
    {
        if (self == NULL)
        {
            return;
        }
        free(self);
        self = NULL;
    }

    В результате запустили тест и получили положительный результат:


    .
    OK (1 tests, 1 ran, 1 checks, 0 ignored, 0 filtered out, 0 ms)

    Это означает, что фаза test-passes завершилась. Далее следует фаза рефакторинга, в которой, как правило, производится улучшение дизайна, читаемости кода и т. д. В нашем случае кода еще совсем мало, поэтому мы только заменили «магическое» число 32 на константу с помощью #define (можно использовать enum или const вместо define).


    Убираем антипаттерн («магическое» число) с помощью #define:


    // Configurator.h
    
    #define SERIAL_RECEIVE_BUFFER_SIZE 32
    
    // Configurator.c
    typedef struct ConfiguratorStruct
    {
        char command[SERIAL_RECEIVE_BUFFER_SIZE];
    } ConfiguratorStruct;

    Итог


    Подведем промежуточные итоги опыта, описанного в этой статье:


    1. Мы определили цели проекта, выбрали аппаратную платформу и инструменты для проектирования.
    2. Подготовили ПК для локальной разработки тестов и кода ВПО без применения специализированных IDE для конкретного МК.
    3. Создали простейший тест, для запуска которого достаточно нажать на кнопку «Run» (или соответствующую горячую клавишу), чтобы мгновенно получить результат выполнения на экране.

    В следующей статье мы напишем всю платформонезависимую логику нашего проекта по методологии TDD в соответствии с разработанным выше тест-листом. Если тебе интересны вопросы «железной» разработки и безопасного кода, присоединяйся к нашей команде. Так что продолжение следует…




    Raccoon Security – специальная команда экспертов НТЦ «Вулкан» в области практической информационной безопасности, криптографии, схемотехники, обратной разработки и создания низкоуровневого программного обеспечения.

    НТЦ Вулкан
    Компания

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

      +2

      Все это к сожалению далеко от практической плоскости разработки для embedded.


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


      Во-вторых проблема эмбеддед разработки в том, что требования выше обычно предъявляются к комбинации ПО+железо, а не к чистому ПО. Потому что сделать спецификацию на ПО многим откровенно лень, да и тестировать нужно все вместе — никому не нужно встраиваемое ПО, которое прекрасно работает в симуляторе, но не работает на реальном железе.


      Ну и в третьих, если учитывать первые два пункта, разработка тестов для такого рода задач становится уже не тривиальной задачей написания кода за 5 минут, а вполне себе отдельным проектом, в котором код сочетается с тестовыми стендами и эмуляторами, которые имитируют внешние воздействия и считывают реакцию системы. И часто процесс разработки такого TDD стоит гораздо дороже разработки самого встраиваемого ПО и поэтому


      в четвертых — обычный стол, пара кнопочек, светодиоды, мультиметр и осциллограф — наши друзья. Иногда ручной тестовый стенд. А автоматические тесты заменяем ручками :-) И это не потому, что так хорошо тестировать встраиваемое ПО, а потому, что по другому гораздо сложнее.

        +5

        В корне с вами не согласен.
        Скорее всего вы путаете системное тестирование (черный ящик) и юнит тестирование (белый ящик) .


        т.е. формализованные требования, которые бы указывали, что ваша железяка вместе с ПО должна делать

        Для юнит тестирование не нужны требования, нужна детальная архитектура. Т.е. описание (спецификация) функции, метода, класса. Вам все равно придется это делать. К вашей конкретной функции никаких требований никто не предъявляет, кроме вас самого… Таким образом, вы вначале сами описываете, что должна делать функция, входные, выходные параметры и можете либо сразу реализовать это, а потом покрыть функцию юнит тестами, либо вначале сделать юнит тесты, а потом сделать реализацию под эти тесты.


        Во-вторых проблема эмбеддед разработки в том, что требования выше обычно предъявляются к комбинации ПО+железо, а не к чистому ПО.

        Опять таки, скорее всего путаница между тестированием черного ящика, и белого. Ваш код функции по сути и есть спецификация функции и чтобы убедиться, что она делает, то, что вы задумали, вы и пишите юнит тест.
        А вот как работает софт и железо вместе — уже пусть системные тесты проверяют, но если у вас нет требований, то тогда и проверять то нечего :)


        Ну и в третьих, если учитывать первые два пункта, разработка тестов для такого рода задач становится уже не тривиальной задачей написания кода за 5 минут

        Она точно такая же задача, как и написание кода. Вы можете точно также писать функцию, потом прошивать микроконтроллер, потом смотреть осцилографом, потом понять, что функция работает не так, снова менять функцию...(по сути будете делать ручной юнит тест). Вопрос зачем и сколько вы на это потратите время? Если все можно сделать юнит тестом?


        Да еще и если потом кто-то поменяет вашу функцию, и она перестанет работать, так как вы задумали, вы даже об этом и не узнаете… Вообще замечательно.


        С последним вашим абзацем почти согласен, юнит тесты используются, чтобы автоматизировать и проверить, что ничего с вашей функцией не случилось, что она работает так как вы задумали.


        Или каждый раз прошивать и проверять, или просто запустить юнит тесты. Профит со всех сторон, экономия времени, ресурсов и затрат.

          +1

          Да, я говорю о системном тестировании, поскольку слово юнит-тестирование в статье не встречается ни разу, а применимость TDD обсуждается в контексте подхода к разработке встраиваемого ПО в целом (типа встраиваемого ПО блока управления тормозной системой, а не одной из функций этого ПО)
          То есть вы хотите сказать, что автор забыл упомянуть этот момент?

            +2

            Дак автор описывает CppUTest — который как бы намекает на то, что это Unit Test Framework.

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

          Во-вторых и в третьих, Обычно для теста разработанной системы «ПО + железо» используют системное тестирование. Действительно, это отдельная задача. В этой статье говорится о методологии TDD с целью разработки бизнес-логики, которая будет работать в конечном продукте. А тестирование системы «ПО + железо» в реальных условиях будем проводить в третьей части цикла статей вручную.
            +1
            Во первых вы не указали, что для того, чтобы писать тесты, в первую очередь нужны спецификации — т.е. формализованные требования, которые бы указывали, что ваша железяка вместе с ПО должна делать. Без них вы можете написать хоть тысячу тестов, которые не будут тестировать то, что вам надо.
            Но код же вы как-то пишите, и явно осознавая, что он должен делать. Наврядли все-таки вы разрабатываете по принципу «напишу что-нибудь как-нибудь, а потом посмотрю, что получится». Соответственно, раз вы можете реализовать функцию или функциональный модуль (классу), значит у вас есть хоть какие-то требования к функции или функциональному модулю (классу), хоть на бумаге, хоть в голове. И следовательно, вы можете написать для них тесты.
            Потому что сделать спецификацию на ПО многим откровенно лень, да и тестировать нужно все вместе — никому не нужно встраиваемое ПО, которое прекрасно работает в симуляторе, но не работает на реальном железе.
            Для этого давно уже придумали разделение по слоям абстракции. Так часть, которая завязана на взаимодействие с железом (HAL) — да, она тестируется отдельно на стендах, а то, что напрямую на железо не завязано (автоматы состояний, математические вычисления, парсеры коммуникационных протоколов, структуры для хранения архивов и логов и т.д.) — отлично тестируется даже без живой железки. И естественно, второе к первому в коде не должно быть прибито гвоздями.
            И часто процесс разработки такого TDD стоит гораздо дороже разработки самого встраиваемого ПО и поэтому
            Да, и это нормально. У известной библиотеки SQLite объем тестов в 600 раз (!) превышает объем кода самой библиотеки.
            Тут уже все зависит от того, что вам нужно получить и как оно будет развиваться. Если у вашего продукта нету требований к надежности работы, если он расширяется и обновляется довольно редко или вообще никогда (сделал и забыл), то да, можно посадить пару студентов за копейки ручками тестить. А если у вас что-то сколь-менее сложное по внутреннему устройству, причем активно развивающееся и постоянно обновляющееся — затраты на постоянные ручные тестирования и цены упущенных ошибок из-за человеческого фактора могут сильно превысить денежно-временные затраты инженеров на написание тестов.
            +2
            В качестве IDE – Visual Studio, потому что нам нравится ее внешний вид, удобство отладки и рефакторинга кода. Данная IDE также подходит для написания кода на «чистом» C.

            А как у нее с С99? Неужели добавили поддержку?

              +1

              Вообще CppUTest можно использовать и с родным компилятором для микроконтроллера, а его можно подключить в IDE Visual Studio.

                0

                Но тогда их надо запускать или в симуляторе родной архитектуры МК, либо на тестовой плате и получать результаты тестов на комп каким-то образом.


                Вообще, я может невнимательно читал, но в статье я не нашел, под какую платформу происходит компиляция и как происходит запуск.
                Мне показалось, что тесты компилируются под х86 и запускаются там же...

                  +1

                  Все верно, по моему опыту 90-95% юнит тестов нормально на симуляторе можно проверить. На IAR все это делается через C-SPY.


                  По поводу х86, да тоже не совсем понял из статьи на какой платформе запускаются тесты, но ничего не ограничивает (кроме ресурсов (например, ОЗУ) микроконтроллера) запустить их на целевой платформе, в данном случае ARM CortexM.

                    +2

                    Мой опыт тестов в симуляторе Keil говорит примерно то же самое; единственное что тесты иногда довольно долго выполняются.

                      0
                      А не могли бы Вы дать чуть больше информации про тестирование в Keil, было бы очень интересно узнать побольше об этой возможности. Может быть, есть что-то, выложенное на Github?
                        0

                        У меня есть старая статья (которая, правда, не совсем про это), сейчас я пользуюсь другим фреймворком; тоже самодельным, но уже на С++, регистрировать тесты гораздо удобнее и boost не нужен.


                        Если коротко, то все сводится к запуску в симуляторе, с перенаправлением вывода printf в отладочное окно.


                        Как-то сходу ничего больше на нашел. Но если хотите, могу статью написать :)

                          0
                          +1 за статью о юнит тестировании в Keil
                      0
                      Так C-SPY это просто расширенная отладка. Или там уже assert можно задавать?
                        0

                        C-SPY чтобы запустить юнит тесты на целевой платформе, либо в симуляторе, либо прямо на плате. Не совсем понял, что вы имели ввиду под задавать assertы. Но результаты с тестов выдаются через Terminal I/O.


                        Подробнее можете здесь прочитать:
                        https://cpputest.github.io/stories.html

                      +2
                      Да, Вы правы, тесты компилируются и запускаются на х86
                    +1
                    Поддержка не полная, поэтому можно использовать другую IDE на свой вкус и цвет, как написано в статье.
                    +2
                    В чем смысл проверки указателя на ноль перед вызовом free() и обнуления указателя перед выходом из функции?
                    void Configurator_Destroy(Configurator * self)
                    {
                        if (self == NULL) // Зачем?
                        {
                            return;
                        }
                        free(self);
                        self = NULL; // Зачем?
                    }
                    Согласно спецификации free() сама должна проводить проверку параметра на неравенство нулю. Обнуление self перед выходом так же не имеет никакого смысла.
                      +3
                      К сожалению, мы живем не в идеальном мире. По нашему опыту не все реализации функции free() соответствуют спецификации, поэтому у нас сформировалось правило всегда проверять указатели на NULL.
                        +2
                        В какой-то древней книжке еще в университете читал, что перед вызовом free обязательно надо проверять указатель, иначе UB. Естественно это может быть в целом ложным утверждением, справедливым только для той конкретной древней книжки и какого-то особенного Си, описанного в ней, но в памяти отложилось навсегда, посему делаю так же всю жизнь…
                          0
                          C99 функция void free(void *ptr)
                          If ptr is a null pointer, no action occurs. — передавать null безопасно.
                        –1

                        Для чего это все на микроконтроллерах, если есть отладка?
                        Как решается проблема взаимодействия с реальным железом?

                          +3

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


                          Отладка может делаться в двух случаях:


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

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


                          К примеру, настраиваете вы модуль SPI на нужные вам настройки (полярность, фазу и так далее) в методе Config(). Вы можете сделать юнит тест, который проверит, что после вызова этого метода установлены нужные биты регистров SPI. И запускать этот тест каждый раз при сборке. Если какой-то программист Снежинка взял и поменял фазу (считывание данных с переднего на задний фронт), у вас и дальше все может продолжить работать, в том числе и под отладкой, и вы не заметите подвоха, а вот пользователь вашего устройства, работающий на -50С уже заметить, так как до этого SPI работал на пределе, небольшой сдвиг от температуры и вуаля, ничего не работает


                          А вот юнит тест это бы увидел — настройка в регистре не та… — тест провалился.


                          При этом возникает резонный вопрос, если вы настройку сделали в полном соответствии с документацией на микросхему, зачем вообще это отлаживать?

                            +6
                            Отладка обычно используется для решения конкретной проблемы. В то время как TDD — это методология разработки, у которой есть определенные преимущества. Применение TDD может сократить время отладки, так как множество ошибок может быть выявлено с помощью тестов. В результате разработчик будет более сосредоточен на написании кода, а не на поиске бага.
                            А проблему взаимодействия с «железом» обсудим в следующей части этого цикла статей.
                              +1
                              Для чего это все на микроконтроллерах, если есть отладка?
                              Все-таки обычно тестирование (не важно, ручное или автоматическое) нужно для того, чтобы убедиться, что все работает как задумывалось на всех предусмотренных сценариях, а отладка — чтобы понять, почему именно оно не работает, как задумывалось на конкретном сценарии.
                              Ну да, можно конечно тестировать отладчиком, но это, как и любое ручное тестирование, очень муторно и медленно. А время — это тоже деньги.
                              Как я уже говорил выше, грамотная архитектура для встраиваемого ПО — это разделение хардварно-зависимой части и основной логики и минимизация жестких связей между ними. Да, если у вас проект — это ардуина с мигающим светодиодиком, то заморачиваться со всем этим TDD нет смысла. А вот если у вас задача сложнее и масштабнее — там это может дать очень хорошую экономию времени (еще раз, время — это деньги) и серьезно понизить вероятность просачивания ошибок в продакшн.
                              +6
                              Хотелось бы в следующей статье увидеть использование данной методологии на конкретном рабочем примере. Например реализуйте простейшую мигалку на ws2812b и покажите что можно отследить тестами, а чего нельзя.
                                –7

                                TDD концепция применима только к тривиальным приложениям. Потому что в более менее средней по сложности системе изначально не понятно как все будет выглядеть в конце, и соответсвенно о каких тестах можно говорить. Сначала пишется прототип, потом он дорабатывается и затем делают рефакторинг и покрывают тестами.
                                Это не только в embedded, а вообще везде в IT. Мне интересно те кто говорит про TDD вообще хоть что то сами писали?
                                p.s.
                                Тем более мне непонятно как TDD совмещается с Agile, TDD это более waterfall model — это реальный парадокс.

                                  +5
                                  Когда пишется что-то, то всегда есть выбор, как это тестировать: отлаживать и проводить системное тестирование в конце уже готового изделия\продукта, инкрементно разрабатывать, и на каждый такой инкремент писать юнит тест, или сначала прорабатывать требования, делать предварительное проектирование модуля\класса\метода, писать для этого функционала-инкремента тест, а потом писать реализацию. Да, действительно, все требования почти никогда не ясны с самого начала. Но ТЗ\требования бизнеса\заказчика присутствуют, а проектирование «нетривиального приложения» всегда есть инкрементный процесс, в самом начале которого идет (явно или неявно) фаза предварительного проектирования и формализация требований на следующее планируемый функционал. Так или иначе, когда программист начинает что-то сложное писать, он обязан понимать, что у него будет на выходе и какую задачу будет решать. Величина того, что он хочет сделать может помещаться в методе\классе или в какой-то большей сущности, но всегда есть требования на нее, а значит, можно и написать тест. Мне интересно, те, кто говорит про неприменимость TDD вообще когда-нибудь пробовали эту методологию или хоть пробовали писать что-то действительно сложное, когда отладка превращается в ад и сжирает намного больше времени, чем разработка?

                                  P.S. Тем более не понятно, как TDD совмещается с waterfall моделью, когда как эта техника требует проведение маленьких итераций разработки.
                                    –1
                                    Мне интересно, те, кто говорит про неприменимость TDD вообще когда-нибудь пробовали эту методологию или хоть пробовали писать что-то действительно сложное, когда отладка превращается в ад и сжирает намного больше времени, чем разработка?

                                    Пробовали, и тут получается палка о двух концах — если упереться в TDD, то на написание тестов и их поддержку может тратиться больше ресурсов, чем на саму разработку, но зато имеем сокращенное время отладки. И наоборот.
                                    То есть в одних случаях может быть выгодно первое, а в других второе. Например, если вносить изменения в прошивки после выпуска не требуется или требуется, но очень редко, то держать наготове TDD фреймворк имеет мало смысла — он просто устареет к тому времени, когда понадобится в следующий раз. А вот если у вас, как у Google, прошивки выходят каждый месяц, то да — есть смысл.

                                      0

                                      Ну так никто не говорит о TDD как о панацее! Вы совершенно правы, иногда это оправдано, иногда — нет. Но возможно)

                                        0
                                        А что у TDD фреймворка может устареть? Мне кажется что вы не совсем поняли концепцию. К примеру, сокращение времени отладки — это не единственный профит от применения TDD, их там небольшой список.
                                        Как уже сказали, TDD и в самом деле не панацея. Но я считаю что это важный и нужный инструмент, приносящий пользу.
                                      0
                                      Потому что в более менее средней по сложности системе изначально не понятно как все будет выглядеть в конце, и соответсвенно о каких тестах можно говорить.
                                      Точно так же, как и при написании кода в условиях изменяющихся требований. Изменились требования — изменили код и изменили тесты, либо изменили тесты и изменили код (если у нас реальное TDD).

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

                                    Самое читаемое