Часто в РФ приходится слышать мнение, что в Firmware разработке якобы в принципе не может быть никакого модульного тестирования. Во всяких военных НИИ даже бытует расхожее мнение

Не нужны никакие тесты. Если программист хороший, то и код он пишет без ошибок.

Попробуем разобраться какие есть плюсы и минусы в модульном тестировании и понять надо оно или нет.

Пролог

Что такое модульный тест? Это просто функция, которая тестируют другую функцию. Это способ получить обратную связь от того кода, который был написан Software In The Loop (SIL). Модульные тесты должен писать прежде всего сам автор основного кода. Также весьма полезны непредвзятые тесты от человека со стороны.

Достоинства модульных тестов

Зачем могут быть нужны модульные тесты?

1--Прежде всего для контроля работоспособности функционала. Тут всё очевидно. Тест доказывает, что какой-то код работает.

*2--Для безопасного перестроения программы (рефакторинга). Часто первый написанный код не самый понятный, переносимый и быстрый. Код просто минимально допустимо работает. При масштабировании оригинальный код однозначно придётся менять, упрощать. Модульные тесты дадут сигнал, если перестройка проекта вышла из-под контроля и что-то рассыпалось. 

*3--Тесты как способ документировании кода. Посмотрите на тест и вы увидите как заполнять прототип функции и вам не придётся писать doxygen для каждой функции или параграфы из комментариев.

Хороший С-код понятен без комментариев.  

*4--Модульные тесты служат критерием завершения работы на конкретном этапе. Разрабатываем код пока не пройдут тесты. Далее принимаемся за следующий программный компонент. Всё четко и понятно. 

*5--Существующие тесты помогут для контроля исполнения работы. Team Lead может написать тесты, а инженер-программист разработает программные компоненты для прохождения этих тестов.

*7--Для снятия ответственности с программиста. Программисты должны быть сами заинтересованы в том, чтобы писать код с тестами. В этом случае они всегда смогут сказать:

Смотрите. Код, который я написал, прошел тесты позавчера. Значит я не причем в том, что сегодня прототип загорелся перед инвесторами

Если в фирме не принято тестировать код, то в такой организации как правило всю вину сваливают на программистов. Вам оно надо?

6--Для покрытия кода. Если есть тест для компонента, значит есть и покрытие кода в этом компоненте. При настроенном измерении покрытия кода можно при помощи модульных тестов выявлять лишний и недостижимый код.

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

Напротив, когда тесты не пишутся, то код получается похож на спагетти: циклопические функции, перемешанный аппаратно-зависимый и аппаратно-независимый код, куча магический циферок и прочее. 

*9--Модульные тесты дадут вам гарантию, что в другом окружении (на другой платформе (PowerPC, AVR, ARC, ARM Cortex-M, x86, RISC-V) и другом компиляторе (IAR, CCS, GCC, GHS, Clang)) функционал будет вести тебя как и прежде и работать как и задумывалось изначально.

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

*10--Модульные тесты полезны для регрессионного тестирования. Для гарантии, что новое изменение не поломало старый функционал. Так как все зависимости предугадать невозможно.

*11--Разработка с тестированием придает процессу создания софтвера положительный азарт. После коммита так и хочется открыть CI и проверить прошли ли все тесты. Если прошли, то наступает радость и гордость за проделанную работу.

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

*12--Прогон модульных тестов ровным счетом ничего не стоит. Тесты можно прогонять хоть каждый день (например во время перерыва).

Напротив, ручное же тестирование программ по check-листу очень дорого так как это человеко-часы. Плюс при ручном тестировании в ход ступает целый калейдоскоп когнитивных искажений свойственный человекам.

*13--Модульные тесты это очень удобная метрика для менеджера проекта. Если тесты проходят и их количество день ото дня возрастает то значит что всё идет хорошо. 

Недостатки модульных тестов

1--Нужна память для хранения кода с тестами. Часто можно услышать высказывание: "Я не буду добавлять тесты в сборку так как у меня мало flash памяти в микроконтроллере". Разруливается эта ситуация очень просто. Если все тесты не помещаются в NorFlash память то делим общее количество тестов на несколько сборок. Записываем их по очереди на Target, прогоняет группы тестов и сохраняем их логи в отчет или генерируем XML таблицу с полным отчетом. Всё это можно легко сделать автоматически в том же CI CD на основе Jenkins. При условии что в прошивке есть загрузчик и интерфейс командной строки Shell.

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

3--Можно написать чрезмерное и избыточное количество тестов. Повторные тесты только с разными именами. Это реальная проблема. Для ликвидации этого по хорошему надо измерять покрытие кода. А это весьма высокоорганизованный процесс. Нужен программный инструментарий для измерения покрытия кода в RunTime прямо на Target(е). Как правило такие технологии платные (Testwell CTC++, LDRA).

Однако есть и второй более простой путь. Писать тесты исходя из технический требований (если они есть). Тогда все понятно. Есть требование будет тест. Нет требования - не будет теста.

4--Модульные тесты не могут протестировать весь нужный функционал. Нужны еще интеграционные тесты. На эту тему есть множество мемов.

Или, например, uTest для дверей в метро. По отдельности они могут отлично открываться, но это не значит, что дверцы откроются одновременно в одну сторону.

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

Общие принципы модульного тестирования

*1--Код отдельно тесты отдельно. Тесты и код разделять на разные программные компоненты. Распределять по разным папкам. Один и тот же тест тестирует разные версии своего компонента.

*2--Каждый тест должен тестировать только что-то одно

*3--Прежде чем начать чинить баг надо написать тест на этот баг. Это позволит отладить сам тест.

*4--Тест должен быть полностью детерминированным. Алгоритм теста должен быть постоянным. Не стоит использовать в тесте генератор случайных чисел. Иначе можно получить тест который, то будет проходить, то не будет.

*5--Тест должен легко пониматься. Юнит тест должен быть простым как табуретка.

6--Тест должен быть коротким

7--У теста минимальные затраты на сопровождение

*8--Тест должен проверять предельные случаи

*9--Тесты должны быть интегрированы в цикл разработки

10--Тест должен легко запускаться. Например по команде из UART CLI.

*11--Тест должен быть устойчив к рефакторингу

12--Тест проверяет только самые важные участки кода

*13--Чинить тесты надо в первую очередь. После продолжать писать production код.

14--Тесты либо работают либо удалены.

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

*16-- Тесты составлять по принципу три A: Arrange, Act, Assert AAA

17-- В модульных тестах не должно быть оператора if в явном виде

*18-- Модульный тест должен воспроизводится. Успешный тесты (зеленые) должны быть успешными при повторном запуске. Красные тесты на бажном коде должны постоянно падать. Результат модульного теста не должен зависеть от случайных величин таких как "угол Солнца над горизонтом или фаза Луны".

*19--Любой предыдущий тест не должен ломать последующий тест. Даже если все тесты по отдельности проходят. То есть любая перестановка порядка исполнения модульных тестов должна проходить успешно.

*20--Модульные тесты надо прогонять на Target устройстве. То есть прямо на микроконтроллере (см HIL). В естественной среде обитания. Запускать тесты можно по команде через UART-CLI/Shell при помощи Putty/TeraTerm

Вывод

Плюсов у модульного тестирования много. Минусов мало и они решаемые. Как по мне модульные тесты на самом деле нужны всем: программистам, team lead(ам) и менеджерам.

Если в сорцах прошивки нет модульных тестов, то это Филькина грамота

Пишите код firmware с тестами.

Links

https://www.youtube.com/watch?v=-hM38-JDt8c

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


  1. sandersru
    17.11.2022 02:19
    +6

    Сколько не думал, так и не смог решить задачу - допустим у нас есть embedded linux. Под капотом есть arm и какая то обвязка. А как собственно оттестировать работу с этой обвязкой? Эмулировать i2c, spi, serial, а поверх прикрутить виртуальные устройства вместо реальных датчиков и устройств.

    Примерно как mook server для http.

    Все, что выше тестируется легко. А вот уровень железных интерфейсов - боль. Не поделитесь?


    1. lorc
      17.11.2022 04:06
      +2

      Так тестировать собственно на реальном железе. Тем более что это модульные тесты, не интеграционные.


    1. Koyanisqatsi
      17.11.2022 09:33
      +1

      Могу высказать гипотезу как тестировать инициализацию и работы периферии в микроконтроллерах ARM.

      Как всё работает с реальным железом?

      Для того чтобы настроить какой-либо периферийный модуль, нужно записать байты в определенные регистры, которые лежат в памяти по определенному адресу. Чтобы иметь доступ к ним, над ними созданы структуры (например CAN_TypeDef, список которых можно найти в заголовочном файле от вендора.

      В таком файле есть такая запись как правило:

      #define CAN1    ((CAN_TypeDef *) CAN1_BASE)

      т.е. какой-то адрес в памяти приводится к указателю на структуру, а потом по этому указателю осуществляется доступ к регистрам периферийного модуля.

      Собственно идея: в тесте создать свой собственный экземпляр структуры CAN_TypeDef. Имея некоторые эталонные значения регистров, их можно сравнивать с тем, что получилось при инициализации в нашей тестовой структуре. Сам такое ещё не пробовал, потому что такой подход подразумевает собой какие-то ненормальные трудозатраты.


      1. sandersru
        17.11.2022 12:41

        Проблема в том, что мне надо гонять фреймворк, драйвера к устройствам по CI в облаке. Т.е там может быть даже не ARM. Надо виртуально создать те же SPI/Serial интерфейсы и виртуальные устройства к ним. И как то оттестировать все.

        Так что регистры сразу мимо. Виртуализация шин и устройств на Линукс нужна.

        Ну то есть код лежит а github и тестируется job-ами на gitthub, где не ARM не подключенных устройств нету.


      1. BigBeaver
        18.11.2022 10:38
        -1

        потому что такой подход подразумевает собой какие-то ненормальные трудозатраты.
        При этом спасает практически только от опечаток.


    1. aabzel Автор
      17.11.2022 13:18
      -1

      Интерфейсы I2C и SPI тестировать очень легко и делать это можно так так:

      1--В обработчике прерываний по окончании отправки увеличивать счетчик отправленных байт.
      2--запустить отправку

      3--подождать прерывания окончания отправки

      4--Убедиться что счетчик отправленных байт увеличился на нужное значение. Если да то тест пройден. Если нет то не было прерывания. Значит интерфейс не работает.

      UART и CAN вообще можно тотально тестировать в режиме loopback.


      Потом. В каждом нормальном I2C/ SPI чипе должен быть константный регистр ChipID. Если он читается в нужное значение значит SPI/I2C работает.


      1. sandersru
        17.11.2022 13:55
        +1

        Может я что-то не понимаю, но давайте это попробуем наложить на кейс:

        1. Есть github и проект в нем. Который раз в день собирается где то в облаке гитхаба.

        2. JOB-а качает ubuntu-latest на armv7 где у нас даже нет даже близко SPI/I2C/UART/etc

        3. Нам надо поднять эти интерфейсы виртуально(либо запилить свой образ где они есть), чтобы наше приложение через read/write/ioctl могло с ним работать.

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

        5. Соответственно in/out должен соответствовать ожидаемому

          Боюсь не прирываний, ни тем более доступа к CPU и регистрам у нас тут нет.

          Сейчас же я вижу исключительно возможность тестировать на конкретном "железе" и никак иначе


        1. lorc
          17.11.2022 16:52
          -1

          У гитхаба есть веб-хуки. Например все pull requests в проект OP-TEE тестируются на реальном железе.


          1. sandersru
            17.11.2022 17:02

            Они свое "реальное железо" в облако github завезли? Установили в стойки датацентров и тестируют? :)

            https://github.com/OP-TEE/optee_os/blob/master/.github/workflows/ci.yml

             builds:
                name: make (multi-platform)
                runs-on: ubuntu-latest
                container: jforissier/optee_os_ci

            Те же контейнеры или qemu


            1. lorc
              18.11.2022 01:14
              +1

              Как же хорошо кичится невежеством, правда? Я ничего не говорил про github workflow. Так что зря вы полезли смотреть именно его. Я говорил про веб-хуки. Если бы вы открыли любой пулл риквест там, то увидели бы проверку от IBART. Вот, например: https://optee.mooo.com:5000/logs/OP-TEE/optee_os/5653/1124289966/b594c241daea2a2592eadb17189591bad9fe8b9c

              И таки да, это исполнялось на реальном железе. И нет, не в стойках датацентра гихтаба.


              1. sandersru
                18.11.2022 02:15
                +1

                Вы правда не читали то на что это написали? Попробуйте ещё раз.

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

                Как бы тут сказать. Все веб программирование научилось виртуализироваться, разработчики чипов - эмулирует работу на fpga. И только в эмбеддед - будь добр припаять провода. Только хардкор.

                А давайте я вам еще усложню задачу... В этом фрэймворке есть эдак 600 интегрированных модулей которые тестируются параллельно.

                Ну например интеграция с Oracle, DB2, MySql, Postgrees, Keycloak, AWS, K8S, Camel и еще очень длинный список.... Ну все вместе потащим куда то на железо? Только виртуализация, образы, контейнеры и где приходит embedded - наступает боль...


  1. Whitech
    17.11.2022 08:29
    +9

    Многообещающий заголовок, а в результате много неплохих тезисов и, собственно, ни слова о модульном тестировании в Embedded.


    1. randomsimplenumber
      17.11.2022 09:30
      +1

      Студенты пишут рефераты? ;)
      Вся надежда на комментарии ;)


      1. F0iL
        17.11.2022 10:04
        +1

        Если интересует как именно стоит тестировать и полезные трюки для этого, рекомендую книгу "Test-Driven Development for Embedded C", автор James W. Grenning.

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


        1. Bobovor
          17.11.2022 10:49
          -1

          Тдд на си это для людей с обилием времени. Как вообще кто то себе это представляет? Проект может собираться десятки минут, это не жс файл исполнить отдельный. Писать на каждый файл таргет в смейке?


          1. F0iL
            17.11.2022 11:27
            +2

            Тдд на си это для людей с обилием времени.

            Расскажите это разработчикам SQLite. У них в проекте 151 тысяча строк кода на чистом Си, и при этом в 608 (!) раз больше кода с тестами (суммарно прогоняются миллионы тестов).

            Писать на каждый файл таргет в смейке?

            Зачем? Издревле существуют тестовые фреймворки для C. Вы объявляете тесты-функции макросами, они все компилируются в тестовый бинарь, а дальше можете запустить все тесты скопом, или какую-то группу тестов, или один конкретный тест.

            Если вам хочется гонять тесты именно на таргете и важен размер бинаря, то стоит хорошо подумать, а надо вам гонять их именно там, и если всё-таки надо, то нет смысла руками писать отдельный таргет для каждого случая - во-первых можно группировать, а во-вторых вполне можно навернуть скрипт для CMake'а который сгенерит все автоматом.

            Проект может собираться десятки минут

            Именно для этого и придумали CI/CD. Запушили код в VCS, тыкнули кнопочку в CI-системе (а по-хорошему вообще стоит настроить pist-commit hook), и занялись другими делами - а оно там пусть по заранее прописанному пайплайну компилируется и тестируется хоть всю ночь, потом получите отчет с результатами (опять же, никто не мешает запустить там только группу тестов или один конкретный тест).

            Бонусом прикрутить туда cppcheck, clang-static-analyzer, clang-format и doxygen и lcov и код будет лучше и чище.


            1. BobovorTheCommentBeast
              17.11.2022 17:48

              1. Разговор был про TDD, не просто тесты. Оно подразумевает частое и постоянное переключение между кодом и тестами, частый их запуск. ЕМНИП там по рекомендациям цикл между запусками должен быть в пару минут. Я пробовал это делать в жабсе, пробовал в шарпе. В JS это скорее даже плюс, с его дурнотой наличие тестов сразу\до даже ускоряет, в C# все еще полезно, но уже начинает задалбывать, т.к. требуется ждать сборку и на это начинает уходит 20-30% времени.
                А вот в С, целый проект пересобирать каждые пару минут уже попросту не возможно. Не говоря уже про то, что сами тесты обычно пишутся на том же самом С и их написание не блещет удобством и скоростью.
                Добавить сюда, что обычно ембеддед экосистема это либо вимо-консольки, либо тормознутые, тупые и неудобные эклипсо-сетебобы с UI курильщика.

              2. Вопрос про CI\CD я думаю отпадает, он не касается конкретно TDD.

              3. "А вот в SQLite", вот давайте на чистоту, тут все любят объективность, науку и ее методы. В свете этого - делать что то, потому, что так слделал кто-то, это дурнота. Не говоря уже про то, что я не видал в статьях про ТДД какие либо ссылки на исследования, говорящие, что они поднимают надежность в среднем на столько-то, удешевляют разработку на столько-то и т.д. и т.п.

                Это все - нам так кажется и это норм, до того момента, пока не начинается "Сделаем так же как у тех крутых ребят, просто потому-что".
                У них могут быть свои причины - как например, что это ответственное приложение, на которое опираются миллионы пользователей, которое постоянно развивается.

                Против типичной железки, которая не будет получать обновлений ПО с огромной долей вероятности вообще.


              1. F0iL
                17.11.2022 18:13

                А вот в С, целый проект пересобирать каждые пару минут уже попросту не возможно

                Речь же про юнит-тесты, не? Тогда зачем пересобирать весь проект? Пересобирается только измененный объектник и сам тест. Если у вас там .c-файлы не в полмиллиона строк и не напихано тонны логики в хедерах, то это делается очень быстро. И даже если у вас какая-то тупая дремучая билд-система которая пересобирает то что не следует, есть ccache который ускоряет такие сборки в разы.


          1. randomsimplenumber
            17.11.2022 11:42
            +3

            Тдд на си это для людей с обилием времени

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

            Писать на каждый файл таргет в смейке?

            Если есть cmake - есть и ctest. Обмазать googletest, например, и запускать там где и собирали.
            Тут другое, код нужно сразу писать кроссплатформенно, и взаимодействие с железом разве что через заглушки.


        1. Indemsys
          17.11.2022 11:34
          +1

          рекомендую книгу "Test-Driven Development for Embedded C", автор James W. Grenning. ...

          Там подробно и с кучей примеров  ...

          Для юнит тестирования там всего один пример!
          Пример тестирования мигания 16-ю светодиодами.

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

          А ещё в книге абсолютно проигнорированы средства аппаратной отладки и тестирования. Метод HIL в этом плане гораздо мощнее.


          1. F0iL
            17.11.2022 11:45
            +1

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

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


            1. Indemsys
              17.11.2022 11:52

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


              1. F0iL
                17.11.2022 11:59
                +1

                Тестирование не может быть больше по объёму чем целевой код.

                Да ладно. Расскажите это разработчикам SQLite. У них в проекте 151 тысяча строк кода на чистом Си, и при этом кода самих тестов в 608 (!) раз больше (суммарно прогоняются миллионы тестов).


                1. Indemsys
                  17.11.2022 12:09

                  А я вам могу привести в пример мою файловую систему STfs, в которой всего один интеграционный тест.
                  И мою систему вы сразу можете применять на STM32, а SQLite если просто так сумеете портировать на STM32, то можете это записать как тринадцатый подвиг Геракла, несмотря на все их тестирование.
                  Ну т.е. ваш пример нерелевантный. Вернее нерелевантный в контексте малых встраиваемых систем, где разработчик один, у него ограничен ресурс времени и ни с кем он в своём коде не конкурирует.


                  1. F0iL
                    17.11.2022 12:30

                    И мою систему вы сразу можете применять на STM32

                    ...потому что вы ее изначально разрабатывали под STM32, ага? Тогда не удивительно.

                    SQLite если просто так сумеете портировать на STM32, то можете это записать как тринадцатый подвиг Геракла

                    Смотря под какую ОС. На STM32 под Linux - вообще изи. Знаю одного чувака, который ради фана портировал SQLite вообще на ПЛК (Scadapack 357 если ничего не путаю), правда ему там пришлось помучиться потому что в рантайме то ли вообще не было mmap(), то ли она работала не как надо.

                    несмотря на все их тестирование.

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

                    нерелевантный в контексте малых встраиваемых систем, где разработчик один

                    Малыми встраиваемыми системами мир Embedded не ограничивается. Да, там вообще обычно софт пишется на коленке в одно лицо и много о чем можно не париться. Но нередко (я бы даже сказал, чаще) можно встретить ровно обратное - когда прошивка какой-то железки пишется целой командой, должна быть совместима с целой россыпью ревизий плат этой железки, возможно работать на разных процессорах (например, когда начиная с какой-то ревизии перешли на другой чип из-за кризиса полупроводников, из-за санкций, или из-за того что перестали влезать в старый), а функционал устройства постоянно развивается и расширяется. А бонусом ещё какие-нибудь специфические требования к надёжности и безопасности. Понятное дело, что там подходы к разработке будут совершенно другие.


                    1. Indemsys
                      17.11.2022 12:42
                      -2

                       Но нередко (я бы даже сказал, чаще) можно встретить ровно обратное - когда прошивка какой-то железки пишется целой командой

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


                      1. F0iL
                        17.11.2022 15:29
                        +1

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

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


                      1. Indemsys
                        17.11.2022 16:46

                        Граница между необходимостью интеграционных и юнит-тестов зависит от сложности программы. 

                        Таким образом вы признали наличие границы. И она как раз чуть выше малых систем. Заметим что автор статьи как раз малыми системами и занят.

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


  1. DarkTiger
    17.11.2022 09:07
    +2

    1--Нужна память для хранения кода с тестами. Часто можно услышать высказывание: "Я не буду добавлять тесты в сборку так как у меня мало flash памяти в микроконтроллере". Разруливается эта ситуация очень просто. Если все тесты не помещаются в NorFlash память то делим общее количество тестов на несколько сборок.

    Разруливается она еще проще - через NFS, если на девайсе есть Линукс и/или сетевая часть. В данном случае и тесты, и код, и логи могут лежать под гитом, даже в одном коммите (на предмет "какого вчера работало, а сегодня нет"). Средства диагностики в данном случае можно взять любые, пересобрав их под свою платформу, размер логов не ограничен и т.п. "git diff" в логах работает волшебно, когда поправил паяльником и посмотрел, что получилось, надо только регуляркой временные метки вырезать (или конфиги логгера править), чтобы diff не срабатывал на различие строк в части микросекунд.


  1. mbait
    17.11.2022 09:49

    Кому интересно про настоящее тестирование в embedded, ищите по "software in the loop", "hardware in the loop", "unit testing LD_preload". А статья - вода, автор не в теме.


  1. F0iL
    17.11.2022 10:00
    +1

    1--Нужна память для хранения кода с тестами.

    На самом деле не обязательно. Юнит-тесты, которые тестируют не низкоуровневое взаимодействие с железом, а логику работы работы программы и ее алгоритмы, имеет смысл гонять и отлаживать вообще не на таргете, а на машине разработчика.

    Плюсы очевидны:

    • Для разработки и тестирования частей не относящихся к железному взаимодецствию не надо насиловать флешку прошивая сам контроллер и вообще не обязательно иметь его под рукой

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

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

    • (последнего пункта нет, просто гениальный онлайн-редактор Хабра в мобильной версии, один раз создав пустой элемент списка удалить его уже не даёт)


    1. DarkTiger
      17.11.2022 11:06

      В симуляторе, том же qemu?


      1. F0iL
        17.11.2022 11:12
        +2

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

        У нас (мы разрабатываем IoT-шлюзы для мониторинга и управления различным электрооборудованием и зарядными станциями для электромобилей) бо́льшую часть компонентов прошивки можно собрать под x86-64 Linux и даже под Windows и отлаживать-тестировать без самой железки и даже без эмулятора, и это офигенно удобно.


        1. DarkTiger
          17.11.2022 20:56
          +1

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


  1. CrashLogger
    17.11.2022 10:17
    +4

    Team Lead может написать тесты, а инженер-программист разработает программные компоненты для прохождения этих тестов.

    Вот с этим проблема та же, что и с ЕГЭ - код пишется не для того, чтобы быть красивым, понятным и эффективным, а для того, чтобы проходить тесты.


    1. F0iL
      17.11.2022 10:25

      Не совсем. К кривому копролиту (как например, описанному в первой части этой статьи) с кучей перекрестных зависимостей и глобальных состояний вы нормально юнит-тесты написать не сможете в принципе. Соответственно, если разработчик хочет/должен писать тесты, он будет вынужден правильно структурировать код и проектировать архитектуру программы.


  1. YDR
    17.11.2022 11:02
    +1

    вот я думал-думал, как мне начать разработку без железа, и предполагал, что придется сделать макет на есп32, общающийся по uart.

    (программа на компьютере на python, в железке на С. тесты скорее всего на компьютере)

    А теперь понял, что надо их по tcp/ip разделить, тем более, что сам только что это прорабатывал.. :-)

    И тесты решил попробовать сделать. Полезная статья.


  1. vadimr
    17.11.2022 11:49

    Для снятия ответственности с программиста. Программисты должны быть сами заинтересованы в том, чтобы писать код с тестами. В этом случае они всегда смогут сказать:

    Смотрите. Код, который я написал, прошел тесты позавчера. Значит я не причем в том, что сегодня прототип загорелся перед инвесторами

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


    1. F0iL
      17.11.2022 11:50
      +1

      Тут скорее речь про то, что вы запушили код, все работало, а на следущий день кто-то запушил ещё что-то и сломал ваш функционал. Соответственно, благодаря тестам сразу будет видно, после чьих изменений оно сломалось - получаем минимальный feedback loop.


      1. vadimr
        17.11.2022 13:41
        +3

        Как вы поняли, что всё работало? Только на основании того, что прошли тесты?

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

        Можно, конечно, как один мой знакомый эмбедщик-программист, говорить, что “программа – вещь стабильная, ищите в железе”, и Рафик невиноватый. Но это не очень конструктивный подход для общего дела.


        1. F0iL
          17.11.2022 15:21
          +1

          Не совсем понятно, с чем вы спорите :) Никто не говорил, что наличие всех тестов в состоянии "Passed" гарантирует что программа корректна и не содержит ошибок. Не гарантирует, этого никто и не обещал. Точно так же, как и бронежилет не гарантирует на сто процентов, что солдат не умрет от пули - но это не повод отказываться от бронежилетов. Речь шла о том, что при хорошем покрытии тестами вероятность отловить регрессию (когда что-то, что выдавало правильный результат при известном наборе входных данных, теперь делает не то, что ожидалось) сразу же после изменений многократно выше, чем без них. Рефакторинг существующего кода с тестами делать гораздо приятнее и спокойнее.


          1. vadimr
            17.11.2022 15:30

            Я спорю конкретно с утверждением:

            Код, который я написал, прошел тесты позавчера. Значит я не причем в том, что сегодня прототип загорелся перед инвесторами

            Я отрицаю причинно-следственную связь между первой и второй его частями.

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


  1. viordash
    17.11.2022 13:12
    +2

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

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


    1. Mirn
      17.11.2022 18:53
      +2

      1. А если спагетти вот прям необходимо?
        Например когда надо вот ни жить ни быть, но получить ещё 5-10% больше скорости?
        Чтобы код просто успевал, например между прерываниями таймера 30к-100кГц.
        В итоге вручную разворачиваются циклы, подбираются перестановки строк независящего кода, порой по месту вписывается асм-блоки? Код изобилует тонким тюнингом опций компилятора чуть ли не на каждую строку и тд.
        В итоге получаем прям воплощение антипаттерна "функцию-бога"
        Вот как такое покрыть модульными тестами?

      1. И в частности как покрыть юнит тестами код вылизанный по работе с конкретными таймингами?

        (просто интересен реальный опыт других опытных людей для таких крайних и "вырожденных" случаев)


      1. viordash
        17.11.2022 21:51
        +1

        1. Возьмите проц помощнее. Ну или разделите спагетти на отдельные методы, с более-менее смысловым разграничением, которое можно покрыть тестами. А компилятор все равно все методы заинлайнит.

        2. А может и не нужно такие методы покрывать тестами? Имхо, и с тестами нужно меру соблюдать.

        Юнит-тесты в основном для логики, те функций возвращающих что-то. Для полноценного тестирования нужен хороший HAL, тогда можно и тайминги тестировать.


        1. BigBeaver
          18.11.2022 10:51
          +1

          Возьмите проц помощнее.
          А вы точно EMBED разработчик?


      1. Indemsys
        18.11.2022 01:10
        +1

        Просто автор статьи немного искажает смысл всего действа .

        В концепции TDD (Test Driven Development) юнит тестирование применяется не для того чтобы протестировать написанный код, а чтобы написать этот код!
        Т.е. совершенно иная логика применения метода.

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

        Это я пересказал основную идею книги "Test-Driven Development for Embedded C"

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


        1. F0iL
          18.11.2022 12:20
          +1

          В концепции TDD (Test Driven Development) юнит тестирование применяется не для того чтобы протестировать написанный код, а чтобы написать этот код!

          В концепции TDD одно другого не исключает :)


          1. Indemsys
            18.11.2022 12:33

            Откуда вы это поняли?

            Тестирование уже написанного кода в TDD называется тестированием Legacy code, т.е. кода без тестов, а не unit тестированием. Это разные процессы.

            По сути статья автора не про unit тестирование, а про некое доморощенное тестирование.

            Давайте строже подходить к определениям , так же строго как вы подходите к чистоте чужого "дурнопахущего" кода.


            1. F0iL
              18.11.2022 13:07
              +1

              юнит тестирование применяется не для того чтобы протестировать написанный код

              TDD вполне допускает, что тесты, написанные для создания нового кода, впоследствии будут использоваться для тестирования этого самого уже написанного кода на регрессии после рефакторинга в будущем, например.


              1. Indemsys
                18.11.2022 14:44

                Вы повторяете ровно то что я сказал. Да, именно так, разработка кода ведётся путём тестирования. Не имеет значение в течении дня все делается или с перерывами в несколько лет. Это все равно юнит тестирование.

                Но если вы код написали, а потом к нему написали тесты, то это не юнит тестирование. Это может быть верификационное тестирование, как с упомянутым вами SQLite. Я не нашёл прямых указаний что SQLite разрабатывается методом TDD. Это значит их тесты - это не юнит тесты.

                С другой стороны юнит тестирование может обойтись без специального тестирующего фреймворка. Достаточно просто придерживаться ручного итеративного тестирования при разработке. Например при разработке считывателя 1-Wire написать модуль сбора статистики по считываниям (длительность, скорость реакции, количество попыток, количество ошибок, среднее, максимум , минимум и т.д.). И это будет юнит тестированием. Вот такое я внедряю прямо в релизное фирмваре. И это считаю удобнее чем отдельный тестировочный фреймворк.


      1. F0iL
        18.11.2022 05:40
        +1

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


      1. randomsimplenumber
        18.11.2022 12:38
        +2

        И в частности как покрыть юнит тестами код вылизанный по работе с конкретными таймингами?

        ifndef TESTING , например. То, что невозможно протестировать - исключить из тестов.


  1. beeptec
    18.11.2022 15:18
    +1

    Мухи отдельно котлеты отдельно. Кода речь идет о QA встраиваемого кода, для этого строится программный тестбенч симулятор (так сегодня поступает 99,9% R&D, который настраивается на весь сценарий того, как скрипт должен себя отрабатывать на уровне MCU. В случае же финальной валидации строится аппаратная фикстура (стенд) под конкретную железяку, далее разрабатывается внешний аппаратный тестбенч, который бегает под внешней тестовой платформой и проверяет Вашу MCU работу встроенного софта и это выглядит примерно так:


    1. aabzel Автор
      19.11.2022 15:17

      Есть и российское решение для авто тестов


      1. beeptec
        19.11.2022 17:00

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