Модульные тесты для проектов Ардуино
«Серьезные» разработчики встраиваемых систем (читай: стмщики) время от времени любят шпынять голозадых «ардуинщиков», у которых среда разработки, помимо всего прочего, не поддерживает даже аппаратные отладчики с точками останова и просмотром значений переменных под курсором мышки или в специальной табличке в реальном времени. Что ж, обвинение вполне справедливо, окошко Монитора последовательного порта (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)
Простой вариант: однофайловый скетч с кодом и тестами
При внедрении тестов в проект Ардуино придется учитывать некоторые особенности её сборочной системы. В простейшем случае проект (скетч) состоит из одного файла с расширением «.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("

Добавить в избранное