Статья
Версия для печати
Обсудить на форуме (4)
Эффективная разработка встроенного ПО через тестирование


Перевод с англ.: (C) Dale, 13.09.2010 — 21.09.2010.
Оригинал статьи: Effective Test Driven Development for Embedded Software.

Резюме

В области индустрии информационных технологий получают распространение методологии эффективного управления рисками в разработке ПО и производства качественного ПО. Однако подобные методы пока не получили распространения в области разработки встроенных систем, особенно для систем с ограниченными ресурсами. Сегодня качество встроенного ПО обычно определяется платформенно-зависимыми инструментами тестирования, ориентированными в первую очередь на отладку. В данной статье мы представляем интегрированный набор фундаментальных концепций и методов, не связанных с платформенно-зависимыми инструментами. Фактически наш подход является движущей силой современной разработки встроенного ПО. Результат применения этих стратегий — хороший дизайн, пригодные для автоматического тестирования системы и значительное уменьшение числа программных дефектов. Примеры с использованием 8-битной системы с 16К программной памяти и 256 байтами ОЗУ наглядно иллюстрируют данные идеи.
Ключевые слова: пригодная для тестирования разработка, микропрограммирование, качество ПО, тестирование ПО.

1. Введение

Наиболее значительные программные сбои попадают в заголовки новостей. Проблема, парализовавшая Денверский международный аэропорт в середине 90-х, так широко освещалась в прессе, что звездам Голливуда оставалось лишь завидовать. В действительности программные ошибки, не вызывающие такого интереса со стороны масс-медиа, происходят постоянно. Они срывают бизнес-планы, омрачают жизнь менеджеров и разработчиков и отрицательно влияют на прибыли компаний всех размеров.
Встроенное ПО стоит особняком в программном мире. Системы IT высокого уровня обычно работают в «чистом» окружении и мало контактируют с физическим миром. Хотя в принципе последствия любой ошибки обходятся дорого, ошибку в ПО персонального компьютера или большом корпоративном приложении обычно относительно легко исправить. Напротив, дефект во встроенном ПО топливного инжектора в автомобиле может вызвать массовый и дорогостоящий отзыв продукции. Гораздо более тяжелые последствия может иметь вполне реальная перспектива человеческой смерти по причине дефекта ПО. Возможности, сложность и распространенность встроенных систем постоянно возрастают, а с ними возрастают возможность и вероятность весьма дорогостоящих дефектов ПО.
Эффективные методологии управления рисками при разработке ПО и производства качественного ПО понемногу начинают укореняться в промышленности. Например, методы под общим названием «гибкие методологии» (“Agile Methodologies”) постепенно обретают сторонников [1]. Многочисленные примеры и наш собственный опыт разработки свидетельствуют о том, что «гибкие» методы уменьшают количество ошибок на порядок и более по сравнению с традиционным подходом. Среди этих методов особенно выделяется TDD. TDD на первый взгляд может показаться нелогичным: он требует написания тестового кода перед тем, как будет написан подлежащий тестированию функциональный код. Применение TDD означает, что в процессе разработки ПО оно в любой момент может быть протестировано автоматизированными средствами. Разработка тестируемого кода в рамках TDD — более важная задача, чем разработка «хорошего» кода, поскольку тестируемый код — это и есть хороший код.
Традиционные стратегии тестирования редко оказывают влияние на конечный код, неудобны в использовании для разработчиков и тестеров, а также зачастую откладывают тестирование на конец проекта, когда ограничения по бюджету и времени препятствуют тщательному тестированию. TDD дает полностью противоположную картину.
Основные шаги при применении TDD:
  • Выделить часть функциональности системы для реализации (одну функцию или метод).
  • Написать тест для проверки этой функциональности.
  • Написать заглушку вместо тестируемого функционального кода (чтобы код теста мог скомпилироваться).
  • Скомпилировать; запустить тест и убедиться, что он выдает ошибку.
  • Дополнить функциональный код.
  • Скомпилировать; запустить тест.
  • Произвести рефакторинг функционального кода.
  • Повторять шаги 6 и 7 до тех пор, пока тест не пройдет, а функциональный код не будет реализован «чисто».
  • Повторять шаги 1-8 до тех пор, пока все функции не будут реализованы.
В данной статье мы исходим из опыта работы с системами с ограниченными ресурсами, которые не могут позволить себе роскошь в виде операционной системы или объектно-ориентированного языка (например, C++ или Java). В таком контексте применение TDD обычно считалось чересчур сложным. Казалось, что непосредственное взаимодействие программы с оборудованием в сочетании с ограниченными ресурсами для запуска тестового инструментария воздвигают непреодолимый барьер. Мы покажем, как применение нового паттерна проектирования ПО и многоуровневой стратегии тестирования вносит эффективность TDD даже в проектирование встроенного ПО нижнего уровня (а после расширения — любой встроенной системы).
Хотя важность тестирования встроенного ПО не подвергается сомнению, тестовые методики обычно тесно привязаны к специфическим инструментам или платформам [2]. Предлагаемые нами здесь концепции не имеют такой привязки.

2. Текущее состояние и недостатки тестирования встроенного ПО

Тестирование на ходу

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

Отладка

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

Конечное тестирование

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

3. Разработка через тестирование

Обзор

Разработка через тестирование буквально переворачивает традиционный цикл разработки/тестирования ПО. В рамках TDD цикл разработки больше не является чередованием написания функционального кода и последующего его тестирования. Вместо этого тестирование управляет разработкой. Разработчик ищет пути сделать систему пригодной для тестирования, в соответствии с этим проектирует, пишет тесты, создает тестовые стратегии — и лишь затем пишет функциональный код, отвечающий требованиям порожденного тестами дизайна [3].
Тестирование может принимать различные формы. На верхних уровнях (например, интеграционное и системное тестирование) полная автоматизация — редкое явление. На нижних уровнях TDD подразумевает полностью автоматизированное модульное тестирование.
При автоматизированном модульном тестировании разработчик сначала пишет модульный тест (тест, который проверяет правильное поведение единственного модуля исходного кода, например, функции или метода [4]), а затем реализует соответствующий функциональный код. Для каждой системной функции к набору автоматизированных тестов добавляется код модульного теста. Постоянно производится полное регрессионное тестирование.
Дальнейшее высокоуровневое тестирование дополняет эти тесты уровня кода. И интеграционное, и системное тестирование обычно следует паттернам традиционной верификации программ. В идеале в них также следовало бы добавить средства автоматизации.
Автоматизированные модульные тесты отлавливают большинство ошибок на нижнем уровне, освобождая человеческий ум для тестирования сложных проблем вроде временных коллизий или незапланированных взаимодействий между подсистемами.
TDD дает множество очевидных преимуществ:
  • Код всегда тестируется.
  • Тестирование является движущей силой дизайна. В качестве побочного эффекта имеется тенденция к улучшению кода, поскольку для создания пригодного для тестирования кода необходимо писать его с меньшей степенью зацепления.
  • Система органично растет по мере увеличения объема знаний о ней.
  • Знания о системе накапливаются в тестах; тесты — это «живая» документация.
  • Разработчики могут добавлять новые функции или изменять существующий код, пребывая в уверенности, что автоматизированное регрессионное тестирование обнаружит ошибки и случаи неожиданного поведения системы.

Преимущества TDD, специфические для встроенного ПО

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

Модульное тестирование встроенного ПО

Паттерн проектирования Модель-Проводник-Оборудование

Паттерны проектирования — это документированные решения типовых проблем в области проектирования ПО. Для языков высокого уровня существует множество паттернов, оформленных в элегантной форме, независимой от языка [5].
При попытке применения TDD ко встроенному ПО наиболее очевидная трудность — это наличие аппаратной части. Если подразумевается запуск тестов в автоматическом режиме, аппаратные функции также должны быть автоматизированы. Поначалу эта задача кажется слишком сложной для экономичной реализации. Достаточно полная симуляция аппаратных событий может потребовать сложных платформеннозависимых инструментов или разработки программируемых аппаратных стендов для тестирования. Однако, используя в качестве примера подходящие паттерны проектирования, возможен и другой подход.
В случае графического интерфейса пользователя (Graphical User Interface, GUI) мы имеем похожую ситуацию. Фактически программирование GUI — это смесь функциональной логики с обработкой событий, внешних по отношению к этой логике. Согласно этой аналогии, асинхронные события и программные интерфейсы экранных виджетов подобны внешним прерываниям и аппаратным регистрам встроенных систем.
Разработчики ПО создали два паттерна для решения проблем, возникающих при проектировании качественных приложений GUI. Паттерны проектирования Модель-Вид-Представление (Model-View-Presenter, MVP) и  Модель-Вид-Контроллер (Model-View-Controller, MVC) эффективно и отчетливо отделяют обработку событий виджета от логики потока управления [6]. В качестве побочного эффекта разделение, которое обеспечивают эти паттерны, позволяет автоматически тестировать код представления GUI без необходимости кликанья мышью на реальных виджетах.
В MVP Вид представляет весьма тонкую обертку вокруг набора виджетов, образующих GUI. Модель хранит внешнее по отношению к виджетам состояние и интерфейсы с программными функциями всей системы, непосредственно не связанными с GUI. Представление ссылается и на Модель, и на Вид и содержит логику представления, необходимую для обработки событий виджетов GUI и смены состояния этих виджетов. При таком разделении Вид может быть «заглушен», или симулирован. Заглушенный Вид под управлением автоматизированных тестов может вызывать события и получать вызовы от Представления. Аналогично может быть заглушена и Модель. Таким образом, логика Представления может быть тщательно протестирована автоматически без использования экранного GUI [7]. При этом подразумевается, что виджеты Представления уже тщательно протестированы поставщиком. Тесты для Представления и Модели создаются и добавляются к автоматизированному тестовому набору. Функциональный код пишется таким образом, чтобы тесты прошли. Финальная верификация пользователем подтверждает, что, помимо тестовой системы, к конечной системе подключено все необходимое.
Для использования во встроенном ПО мы разработали паттерн Модель-Проводник-Оборудование (Model-Conductor-Hardware, MCH) по образу и подобию MVP и MVC. Согласно этому паттерну, подобно его собратьям из GUI, MCH позволяет использовать заглушки для физического оборудования, способствует отделению функциональной логики от оборудования и обеспечивает основу для наборов автоматизированных модульных тестов для тестирования как оборудования, так и функциональной логики.



Рис. 1. Отношения между Проводником и остальными членами триады MCH и их заглушками. Изображенные заглушки замещают конкретные члены триады и позволяют тестировать логику с Проводником. Mock Проводник (не изображен) позволяет тестировать конкретные Модель и Оборудование. Глобальные переменные в составе заглушек хранят параметры функций и симулируют возвращаемые значения, используемые в тестовых утверждениях.

Модель

Модель в MCH моделирует текущее состояние системы. Например, если аналоговый выход установлен в +5V, но цепь обратной связи дает +4V (при допуске 100mV), Модель установит внутри себя соответствующее состояние ошибки для использования Проводником. Таким образом, Модель обеспечивает внутреннюю согласованность состояний. Модель взаимодействует только с Проводником и не имеет непосредственной связи с Оборудованием.

Оборудование

Оборудование в MCH представляет тонкую прослойку вокруг собственно оборудования как такового. Этот член триады MCH инкапсулирует используемые системой порты и регистры. Подпрограммы обслуживания прерываний (Interrupt Service Routines, ISR) оповещают Проводник об изменении состояния системы. Оборудование связано только с Проводником и не имеет прямой связи с Моделью.

Проводник

Проводник содержит основную логику управления в триаде MCH. Проводник приводится в действие Оборудованием для обработки новых данных и событий. По этим воздействиям Проводник изменяет состояние Модели и использует состояние, полученное от Модели, в своей логике отправки команд или данных на Оборудование. Проводник содержит управляющий цикл и служит посредником между Моделью и Оборудованием. Проводник назван так из-за своей координирующей роли в системе, а также из-за своей близости к настоящим электрическим компонентам.
Для простых систем может оказаться достаточной единственная триада MCH. Зачастую для упрощения логического разделения тестов необходимо несколько триад. В этих случаях триады обычно существуют независимо друг от друга, а центральный «Исполнитель» вызывает цикл управления каждого Проводника. Если имеется пересечение между триадами, обычно это происходит на уровне Оборудования.

Тестирование с MCH

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

Фреймворк модульного тестирования

Фреймворки модульного тестирования сегодня существуют почти для всех высокоуровневых языков общего назначения. Механизм тестового фреймворка относительно прост в реализации [8]. Фреймворк, помимо функционального, содержит тестовый код, обеспечивает функции для сравнения полученного от тестируемого функционального модуля результата с ожидаемым, а также собирает результаты тестов со всего тестового набора и составляет по ним отчет.
Мы приспособили к своей работе проект с открытым кодом Embunit (Embedded Unit), а также создали очень легковесный фреймворк, который назвали Unity [9]. И Embunit, и Unity — основанные на языке C фреймворки, которые мы при необходимости модифицируем под целевые платформы.

Цена использования MCH

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

TDD встроенного ПО

Четырехуровневая стратегия тестирования

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

1) Автоматизированное модульное тестирование

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

2) Уровень тестирования оборудования

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

3) Тестирование коммуникационного канала

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

4) Сквозное системное тестирование

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

Непрерывная интеграция

Технология непрерывной интеграции регулярно собирает воедино код системы (возможно. от нескольких разработчиков) и посредством регрессионных тестов удостоверяет, что новый код не является причиной отказа ранее написанного кода. Системы автоматизированного построения проекта позволяют автоматически компилировать и запускать программу и тесты. Такие идеи и инструменты — весьма важная поддержка для эффективности TDD, но эта тема лежит за рамками данной статьи [10].

Примеры MCH в области встроенных систем

MCH в среде, ориентированной на C

Создание заглушек и тестов в среде, ориентированной на встроенный C, выполняется посредством mock.o — скомпилированной реализации функций, объявленных в заголовочном файле. Например, предположим, что hardware.h объявляет все функции интерфейса аппаратных средств некоторого микроконтроллера. В этом примере тесты Проводника проверят, что Проводник вызывает специфические функции оборудования с подходящими параметрами. Также будет написан файл определений mockhardware.c, содержащий ничего не делающие функции, которые лишь сохраняют значения формальных параметров при вызове или возвращают специфические значения, как определено глобальными переменными. Объектные файлы для mock'ов и функционального кода компонуются вместе, и тесты получают доступ к вышеупомянутым глобальным значениям для проверки обращений Проводника к интерфейсу mockhardware.h.

Примеры кода

Следующие блоки кода — это примеры, взятые из реального проекта, разработанного для фирмы Savant Automation в Grand Rapids, Michigan. Savant выпускает тележки, управляемые автоматикой. Эти примеры относятся к специальной плате управления скоростью; показанные функции задают напряжение на двигателе. В данном сценарии тестирования мы иллюстрируем тестирование Проводника. Тесты, приведенные в примере, проверяют правильность использования Проводником аппаратного интерфейса (обратите внимание на использование макроса ASSERT из тестового фреймворка).

Код: (C) hardware.h

/// Получить обратную связь от аналогового выхода привода.
millivolts Hardware_GetFeedbackVoltage(void);

/// Задать выходное напряжение на двигателе.
void Hardware_SetOutputVoltage(millivolts output);

/// Установить флаг ошибки.
void Hardware_SetError(bool err);
 

Код: (C) model.h

typedef struct _ModelInstance {
  millivolts FeedbackVoltage;
  millivolts OutputVoltage;
  bool Error;
} ModelInstance;

/// Установить напряжение обратной связи.
void Model_SetFeedbackVoltage(
millivolts feedback);

/// Получить выходное напряжение привода для оборудования.
millivolts Model_GetOutputVoltage(void);

/// Получить состояние ошибки.
bool Model_GetError(void);
 

Код: (C) conductor.h

/// Callback для аппаратного напряжения обратной связи.
void Conductor_HandleFeedbackVoltage(void);

/// Цикл управления, вызываемый из main().
void Conductor_Run(void);
 

Код: (C) mockhardware.h

/// Выходное напряжение обратной связи
extern millivolts Hardware_InputFeedbackVoltage;

/// Выходное напряжение, устанавливаемое проводником.
extern millivolts Hardware_OutputDriveOutputVoltage;

/// Error flag
extern bool Hardware_OutputError;
 

Код: (C) mockhardware.c

#include "mockhardware.h"
#include "hardware.h"
#include "conductor.h"

millivolts Hardware_InputFeedbackVoltage;
millivolts Hardware_OutputDriveOutputVoltage;
bool Hardware_OutputError;

millivolts Hardware_GetFeedbackVoltage(void) {
  return Hardware_InputFeedbackVoltage;
}

void Hardware_SetOutputVoltage(millivolts output){
  Hardware_OutputDriveOutputVoltage = output;
}

void Hardware_SetError(bool err) {
  Hardware_OutputError = err;
}
 

Код: (C) mockmodel.h

/// Моделируемое напряжение обратной связи системы.
extern millivolts Model_FeedbackVoltage;

/// Моделируемое выходное напряжение системы.
extern millovolts Model_OutputVoltage;

/// Моделируемое состояние ошибки.
extern bool Model_Error;
 

Код: (C) mockmodel.c

// Компонуется с testconductor.o вместо model.o,
// чтобы позволить тестировать проводник
// независимо от логики реальной модели.

#include "mockmodel.h"

millivolts Model_FeedbackVoltage;
millovolts Model_OutputVoltage;
bool Model_Error;

void Model_SetFeedbackVoltage(millivolts feedback) {
  Model_FeedbackVoltage = feedback;
}

millivolts Model_GetOutputVoltage(void) {
  return Model_OutputVoltage;
}

bool Model_GetError(void) {
  return Model_Error;
}
 

Код: (C) model.c

#include "model.h"

ModelInstance Model;

void Model_SetFeedbackVoltage(millivolts feedback) {
  Model.FeedbackVoltage = feedback;

  if(feedback != Model.OutputVoltage) {
    // использовать номинальное значение
    Model.Error = true;
  }
}

millivolts Model_GetOutputVoltage(void) {
  return Model.OutputVoltage;
}

bool Model_GetError(void) {
  return Model.Error;
}
 

Код: (C) conductor.c

#include "model.h"
#include "hardware.h"

void Conductor_HandleFeedbackVoltage(void) {
  Model_SetFeedbackVoltage(Hardware_GetFeedbackVoltage());
}

void Conductor_Run(void) {
  Hardware_SetError(Model_GetError());

  if(!Model_GetError()) {
    Hardware_SetOutputVoltage(Model_GetOutputVoltage());
  }
}
 

Код: (C) testconductor.c

#include "conductor.h"
#include "mockhardware.h"
#include "mockmodel.h"

static void testHandleFeedback(void) {
  Hardware_InputFeedbackVoltage = 7;

  Conductor_HandleFeedbackVoltage();

  TEST_ASSERT_EQUAL_INT( 7, Model_FeedbackVoltage);
}

static void testConductorRun(void) {
  Model_Error = false;
  Model_OutputVoltage = 78;

  Conductor_Run();

  TEST_ASSERT_MESSAGE(Hardware_OutputError == false, "Error set incorrectly");
  TEST_ASSERT_EQUAL_INT(78, Hardware_OutputDriveOutputVoltage);

  Model_Error = true;
  Model_OutputVoltage = 99;

  Conductor_Run();

  TEST_ASSERT_MESSAGE(Hardware_OutputError == true, "Error not set");
  TEST_ASSERT_EQUAL_INT(78, Hardware_OutputDriveOutputVoltage);
}
 

Заключение, дальнейшая работа

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

Благодарности

Авторы благодарят Мэта Вернера из Savant Automation за предоставленную возможность реализовать идеи, лежащие в основе данной статьи, в реальной промышленной системе.

Ссылки

[1] Kent Beck, Extreme Programming Explained, Reading, MA: Addison Wesley, 2000.
[2] Wolfgang Schmitt. “Automated Unit Testing of Embedded ARM Applications.” Information Quarterly, Volume 3, Number 4, p. 29, 2004.
[3] David Astels, Test Driven Development: A Practical Guide, Upper Saddle River, NJ: Prentice Hall PTR, 2003.
[4] “Unit Test.” http://en.wikipedia.org/wiki/Unit_test. (Аналогичная статья на русском языке).
[5] Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software, Reading, MA: Addison-Wesley Professional Computing Series, 1995.
[6] Martin Fowler. “Model View Presenter.” http://www.martinfowler.com/eaaDev/ModelViewPresenter.html. July 2004.
[7] M. Alles, D. Crosby, C. Erickson, B. Harleton, M. Marsiglia, G. Pattison, C. Stienstra. “Presenter First: Organizing Complex GUI Applications for Test-Driven Development,” accepted at Agile 2006 conference, Minneapolis, MN.
[8] Kent Beck, “Simple Smalltalk Testing: With Patterns.” http://www.xprogramming.com/testfram.htm.
[9] http://www.atomicobject.com/embeddedtesting.page (в настоящий момент ссылка недоступна).
[10] “Continuous Integration.” http://en.wikipedia.org/wiki/Continuous_integration. (Аналогичная статья на русском языке).
Версия для печати
Обсудить на форуме (4)