Модульные тесты для проектов Ардуино

«Серьезные» разработчики встраиваемых систем (читай: стмщики) время от времени любят шпынять голозадых «ардуинщиков», у которых среда разработки, помимо всего прочего, не поддерживает даже аппаратные отладчики с точками останова и просмотром значений переменных под курсором мышки или в специальной табличке в реальном времени. Что ж, обвинение вполне справедливо, окошко Монитора последовательного порта (Serial Monitor) плюс Serial.println — не самый лучший инструмент отладки. Однако грамотный ардуинщик сможет с легкостью парировать атаку и поставить зарвавшегося стмщика на место в том случае, если он (ардуинщик) использует модульные тесты.

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

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

При подготовке к внедрению в проект модульных тестов следует иметь ввиду:

  • Тесты требуют дополнительного времени для написания кода (на самом деле, нет: время, потраченное на автоматические тесты, вполне сравнимо со временем, потраченным на ручную отладку того же участка, а на долгой дистанции оно еще многократно окупится), при этом код теста может превышать по размеру код тестируемого участка.
  • В покрытом тестами проекте может быть сложно проводить глобальную реорганизацию кода (рефакторинг) — особенно актуально на начальном этапе разработки, когда кодовая база и внутренний API еще не достаточно устаканились (с другой стороны, рефактор проекта, не покрытого тестами, повлечет все те же регрессии, просто вы про них не узнаете)
  • Нужно писать модули приложения так, чтобы их можно было запускать как в рамках приложения, так и внутри отдельных тестов
  • Необходимо проработать структуру и связи внутри проекта так, чтобы в нем нашлось место коду основного приложения, исполняемой прошивке основного приложения, коду тестов, исполняемой прошивке («запускальщик»/ланчер) для запуска тестов.

Я более не буду распространяться про философию модульного тестирования, а просто покажу, как технически внедрить простые модульные тесты в ваш проект на Ардуино.

Далее рассмотрим:

  • Несколько стратегий организации рабочего пространства проекта с модульными тестами с учетом особенностей платформы Ардуино.
  • Вариант «все в одном» (и код и тесты в одном файле скетча),
  • вынесение тестов в отдельный модуль в каталоге скетча,
  • вынесение тестов в отдельный проект.
  • Запуск тестов на устройстве,
  • запуск этих же тестов на настольном компьютере без загрузки на устройство, заглушки для API Ардуино

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

Нам нужен фреймворк модульного тестирования:

  • Для Си/С++
  • Должен работать на устройствах семейства Ардуино
  • Должен работать на настольных системах
  • Люблю легковесные библиотеки (моё персональное предпочтение)

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

Я просмотрел несколько фреймворков и остановился на 2х:

  • ArduinoUnit: https://github.com/mmurdoch/arduinounit. В общем, он удовлетворяет ключевым исходным требованиям: работает как на Ардуино (очевидно из названия), так и на настольных системах (см раздел «En Vitro Testing» на сайте проекта), но на беглый взгляд показался тяжеловатым и я решил посмотреть другие варианты.
  • Библиотека Sput (Sput Unit Testing Framework for C/C++) https://www.use-strict.de/sput-unit-testing/. Это библиотека легкая настолько, насколько это возможно: всего один заголовочный файл, даже без пары с исходником «.cpp» (все сделано на нескольких макросах). Однако вывод сообщений идет через std::out (что совершенно естественно для libc), который на Ардуино как раз не реализован.

И все-таки мои симпатии перевесили в пользу sput, а проблему с std::out удалось решить несколькими исправлениями (заменой printf на sprintf+Serial.print).

В итоге получился проект sput-ino — порт библиотеки sput на платформу Ардуино с сохранением совместимости с настольными системами с libc

— пример однофайлового скетча с тестами
/sput-ino/examples/sput-ino-monolith/

— пример с разделением основного кода и тестов на модули
sput-ino/examples/sput-ino-modules/

— запуск тестов на настольной системе
sput-ino/example-desktop/

— пример с разделением основного кода и тестов на разные проекты — в отдельном репозитории
https://github.com/sadr0b0t/sput-ino-demo

Установим библиотеку

Просто клонируйте репозиторий git https://github.com/sadr0b0t/sput-ino.git в каталог $HOME/Arduino/libraries:

cd $HOME/Arduino/libraries/ git clone https://github.com/sadr0b0t/sput-ino.git

и перезапустите среду Ардуино IDE.

Или на странице проекта github https://github.com/sadr0b0t/sput-ino/ нажмите кнопку Клонировать или скачать > Скачать ZIP (Clone or download > Download ZIP), после этого установите архив sput-ino-master.zip через меню установки библиотек Ардуино: Скетч > Подключить библиотеку > Добавить .ZIP библиотеку….

Примеры появятся в меню Файл > Примеры > sput-ino (File > Examples > sput-ino)

Простой вариант: однофайловый скетч с кодом и тестами

image

При внедрении тестов в проект Ардуино придется учитывать некоторые особенности её сборочной системы. В простейшем случае проект (скетч) состоит из одного файла с расширением «.ino». При сборке файл «.ino» с незначительными изменениями конвертируется в «.cpp» (подключается заголовок Arduino.h и еще кое-чего по мелочи), сгенерированный файл компилируется в прошивку.

Создаем новый скетч
sput-ino/examples/sput-ino-monolith/sput-ino-monolith.ino

добавляем какой-то полезный код:

/**  * @return a плюс b  */ int a_plus_b(int a, int b) {     return a + b; }  /**  * @return a минус b  */ int a_minus_b(int a, int b) {     return a - b; }  /**   * Включить лампочку, если число четное  * @param pin номер ножки лапмочки  * @param num число  * @return true, если число num четное  */ bool led_on_even(int pin, int num) {     if(num % 2 == 0) {         digitalWrite(pin, HIGH);     } else {         digitalWrite(pin, LOW);     }     return num % 2 == 0; }

Пишем тесты с библиотекой sput (подробнее документация: http://www.use-strict.de/sput-unit-testing/tutorial.html):

#include "sput.h"  /** Test a_plus_b call */ void test_a_plus_b() {     sput_fail_unless(a_plus_b(2, 2) == 4, "2 + 2 == 4");     sput_fail_unless(a_plus_b(-2, 2) == 0, "-2 + 2 == 0");      // this one would pass on 32-bit controllers and would fail on AVR with 16-bit int     sput_fail_unless(a_plus_b(34000, 34000) == 68000, "34000 + 34000 == 68000"); }  /** Test a_minus_b call */ void test_a_minus_b() {     sput_fail_unless(a_minus_b(115, 6) == 109, "115 - 6 == 109");     sput_fail_unless(a_minus_b(13, 17) == -4, "13 - 17 == -4"); }  /** Test test_led_on_even call */ bool test_led_on_even() {     pinMode(13, OUTPUT);      sput_fail_unless(led_on_even(13, 2), "num=2 => led#13 on");     // would pass on desktop, might fail or pass on difference devices     // (e.g.: Arduino Due - fail, ChipKIT Uno32 - pass)     sput_fail_unless(digitalRead(13) == HIGH, "num=2 => led#13 on");      sput_fail_unless(!led_on_even(13, 5), "num=5 => led#13 off");     sput_fail_unless(digitalRead(13) == LOW, "num=5 => led#13 off");      sput_fail_unless(led_on_even(13, 18), "num=18 => led#13 on");     sput_fail_unless(digitalRead(13) == HIGH, "num=18 => led#13 on"); }

Комплектуем наборы тестов (тест-сьюты).

Все тесты в одном наборе:

/** All tests in one bundle */ int mylib_test_suite() {     sput_start_testing();      sput_enter_suite("a plus b");     sput_run_test(test_a_plus_b);      sput_enter_suite("a minus b");     sput_run_test(test_a_minus_b);      sput_enter_suite("led on even");     sput_run_test(test_led_on_even);      sput_finish_testing();     return sput_get_return_value(); }

и по одному набору на каждый тест:

/** Test suite for a_plus_b call */ int mylib_test_suite_a_plus_b() {     sput_start_testing();      sput_enter_suite("a plus b");     sput_run_test(test_a_plus_b);      sput_finish_testing();     return sput_get_return_value(); }  /** Test suite for a_minus_b call */ int mylib_test_suite_a_minus_b() {     sput_start_testing();      sput_enter_suite("a minus b");     sput_run_test(test_a_minus_b);      sput_finish_testing();     return sput_get_return_value(); }  /** Test suite for led_on_even call */ int mylib_test_suite_led_on_even() {     sput_start_testing();      sput_enter_suite("led on even");     sput_run_test(test_led_on_even);      sput_finish_testing();     return sput_get_return_value(); }

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

Запускаем тесты здесь:

void run_tests() {     Serial.println("

FavoriteLoadingДобавить в избранное
Posted in Без рубрики

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

Ваш e-mail не будет опубликован. Обязательные поля помечены *