Как написать лучшие тесты для встроенного программного обеспечения с TDD
Хотите попробовать выполнить тестирование вашего встроенного программного обеспечения »// www.allaboutcircuits.com/technical-articles/unit-tests-can-help-you-write-better-embedded-software…-heres-how/" target = " _blank "> преимущества модульного тестирования для себя, вы должны начать с разработки, основанной на тестах (TDD).
Что такое TDD?
Тестируемая разработка (TDD) - это итеративный процесс написания программного обеспечения, где модульные тесты разрабатываются непосредственно перед реализацией. Это замкнутый цикл обратной связи, состоящий из следующих этапов:
- Напишите единичный тест, посмотрите, как он сработает.
- Напишите достаточно кода для прохождения теста.
- Улучшите код (не изменяя его поведение).
Эти шаги часто называются «красным, зеленым, рефактористом» для того, как тесты проходят от отказа (красного) до прохождения (зеленый), с окончательной возможностью улучшить код и тесты (рефакторинг). Во время развития этот цикл повторяется снова и снова сотни или тысячи раз.

В этом процессе написание тестов - это то, что стимулирует разработку программного обеспечения. Вы думаете о том, что вы хотите, чтобы код делал, прежде чем писать, и вы сохраняете эту идею в модульном тесте. Только тогда вы напишете следующий бит кода. Это заставляет вас четко понимать, что вы хотите сделать.
С каждым проходящим тестом вы создаете немного больше уверенности в правильности работы вашего программного обеспечения. И, поскольку каждый бит кода управляется тестом, вы получаете отличное тестовое покрытие - количество вашего кода, которое тестируется с помощью модульных тестов.
Не тратьте время на то, чтобы писать непроверяемый код
Одна из проблем с модульными тестами - особенно когда вы только начинаете, - это то, что вы можете написать код, который трудно проверить.
Например, возможно, у вас есть внутреннее состояние, к которому вам нужно получить доступ, но вы не хотите его раскрывать. Или, может быть, ваш тестируемый блок имеет множество сложных зависимостей, которые сложно издеваться.
Написание кода, который можно тестировать, требует опыта, но как вы можете это получить? Ну, оказывается, вам не нужен этот опыт, если вы начинаете с TDD. Когда вы сначала пишете тесты, вы не можете писать непроверяемый код.
С самого начала вам удастся добиться успеха, и поэтому вы скорее всего примете модульное тестирование как практику. Представьте себе два сценария:
Сценарий 1. Вы пишете целую кучу кода, затем попытаетесь выяснить, как его проверить. Когда вы не можете понять это быстро, вы сдаетесь, потому что у вас есть программное обеспечение для отправки! Возможно, вы узнаете что-то о том, как сделать ваш код более пригодным для тестирования в следующий раз.
Сценарий 2. У вас есть идея создать какой-либо программный модуль, но вы не знаете, как его протестировать. Итак, вы потратите немного времени на выяснение того, как написать первый тест. Затем вы напишите код, чтобы он прошел. Хорошо! Вы только что написали свой первый модульный тест. Хорошая работа, вы только что узнали. Повторяйте, пока не будет полностью протестирован модуль. Поздравляем … вы так много узнали об модульном тестировании.
TDD - это усилитель опыта. Вы учитесь, делая. TDD поощряет вас делать правильные вещи, поэтому вы учитесь быстрее. Чем больше вы учитесь, тем лучше вы сможете писать модульные тесты.
Тестовый подход
При тестировании вы думаете о коде, который вы пишете немного по-другому. Вместо того, чтобы пытаться отслеживать все, что вы хотите от своего программного обеспечения, вы просто беспокоитесь о следующем, что вы хотите, чтобы ваше программное обеспечение делало. Давайте посмотрим на пример для иллюстрации.
Одним из моих любимых примеров для обсуждения TDD является парсер команд, поскольку он используется во многих встроенных системах. Часто вы хотите, чтобы ваша система могла разговаривать с внешним миром, чтобы она действительно могла делать интересные вещи. Это может быть просто простой последовательный интерфейс, используемый для конфигурации, или это может быть подключение к другому устройству или, возможно, к Интернету.
По моему опыту, эти типы интерфейсов могут действительно выиграть от модульного тестирования. Они, как правило, настраиваются на заказ и могут быстро усложняться - с большим количеством путей через код и многими случаями ошибок для обработки. И, поскольку это внешний интерфейс к системе, вы не всегда можете ожидать, что парень на другом конце будет хорошо себя вести. Однако с некоторыми модульными тестами вы можете убедиться, что все работает так, как ожидалось, - и все случаи ошибок обрабатываются.
Рассмотрим встроенную систему с простым парсером команд. Он берет поток символов от где-то (может быть, серийный или USB, например, но наш парсер действительно не заботится) и что-то делает, когда получена определенная последовательность символов. В этом случае в системе есть динамик, который можно контролировать.

Первым инстинктом для большинства встроенных разработчиков программного обеспечения было бы начать писать целую кучу кода в command_parser.c. Подход, основанный на испытаниях, отличается.
Первый шаг: написать тест, посмотреть, как он сработает. Чтобы написать тест, вам нужно выяснить, что вы хотите, чтобы ваш парсер команды делал. Если есть спецификация протокола (га, правильно!), Вы можете взглянуть на это. Если нет, вы можете сразу решить, что вам нужно для выполнения кода в первую очередь. Как насчет этого?
Когда принимается символ «m», динамик отключается.
Хорошо, это простая, маленькая и четко определенная функциональность. Давайте напишем единичный тест, который пройдет, если бы был реализован код для этого.
#include "some_test_framework.h" #include "some_mock_framework.h" #include "command_parser.h" #include "mock_speaker.h" // A test for the command_parser. void test_WhenAnMIsReceived_ThenTheSpeakerIsMuted(void) { // Receive an "m." command_parser_put_char('m'); // Make sure the mute function is called. EXPECT_CALL(speaker_mute()); }
Whoa, это всего лишь один тест, но здесь есть немало проектных решений.
Для парсера команд есть новая функция:
command_parser_put_char()
. Вот как символы передаются в синтаксический анализатор команд и как «m» передается для теста.

Существует еще одна новая функция, которая была определена для модуля динамика:
speaker_mute()
. Это то, что будет делать фактическое приглушение громкоговорителя. Вы знаете, что тест прошел, когда эта функция была вызвана.
Поскольку это единичный тест, командный_параметр будет протестирован изолированно, и реальная версия speaker_mute () не будет вызываться. Вместо этого будет предоставлена макетная функция (возможно, она включена в
mock_speaker.h
), а макрос
EXPECT_CALL
- это резервный механизм для любого изнашиваемого механизма. Это не
speaker_mute()
функция
speaker_mute()
не вызывается.
Обратите внимание, что ни одна из этих функций на самом деле не существует. Но … вы только что определили точное поведение, которое хотите, и у вас есть явный способ проверить его. Если бы вы сейчас проверили тест, это, безусловно, потерпит неудачу. На самом деле, он не скомпилируется, потому что функции не существуют.
Теперь для второго шага: достаточно написать код для прохождения теста. Наконец-то настало время написать код! Вот простейший бит кода, необходимый в
command_parser_put_char()
чтобы пройти тест:
// Receive a character. void command_parser_put_char(char next_char) { speaker_mute(); }
Обратите внимание, что вам также необходимо настроить свой макет для
speaker_mute()
. Детали этого будут зависеть от того, как вы используете mocks в своем проекте.
Тест должен пройти сейчас … но заметьте, что мы даже не проверяем, какой персонаж мы получили! Это может показаться глупым, но одна из целей TDD - максимизировать объем работы, которую НЕ выполняли.
Сейчас это тривиальный пример. Однако, когда код становится более сложным, любой код, который вы на самом деле не пишете, делает ваше приложение более простым и понятным (лучше …). И когда вы выполняете столько же работы, сколько вам нужно, люди, которые заботятся о расписании и бюджете, тоже счастливы.
Заключительным этапом цикла TDD является рефакторинг, где вы улучшаете код, не изменяя его поведение. Ключом к этому шагу является то, что у вас уже есть модульные тесты, которые проверяют поведение. Таким образом, вы можете поэкспериментировать с изменением кода, потому что неудачный тест сразу скажет вам, измените ли вы поведение. Поскольку это всего лишь первый тест, тем не менее, пока еще мало что можно улучшить.
Остальная часть анализатора команд реализована путем повторения цикла TDD. Итак, что вы хотите, чтобы ваш парсер команды делал дальше? Как насчет:
Когда принимается символ «u», динамик не включается.
Хорошо, это еще один хороший. Вот тест:
void test_WhenAUIsReceived_ThenTheSpeakerIsUnmuted(void) { // When command_parser_put_char('u'); // Then EXPECT_CALL(speaker_unmute()); }
Когда вы улучшите реализацию синтаксического анализа команды до прохождения теста, это может выглядеть примерно так:
void command_parser_put_char(char next_char) { if (next_char == 'm') { speaker_mute(); } else { speaker_unmute(); } }
Как теперь обрабатывать ошибку? Что делать, если получен неожиданный символ?
Когда получен неожиданный символ, состояние молчания динамика не изменяется.
void test_WhenAnUnexpectedCharIsReceived_ThenTheSpeakerMuteStateIsUnchanged(void) { // When command_parser_put_char('!'); // Then DO_NOT_EXPECT_CALL(speaker_mute()); DO_NOT_EXPECT_CALL(speaker_unmute()); }
И вот достаточно кода, чтобы пройти этот тест:
void command_parser_put_char(char next_char) { if (next_char == 'm') { speaker_mute(); } else if (next_char == 'u') { speaker_unmute(); } }
Есть что-то, что вы хотите реорганизовать еще здесь? Если вы предпочитаете оператор switch, вы можете пойти и изменить его:
void command_parser_put_char(char next_char) { switch(next_char) { case 'm': speaker_mute(); break; case 'u': speaker_unmute(); break; default: break; } }
Хм, это изменило что-нибудь? Нет пота, просто запустите свои тесты, чтобы узнать.
Отсюда вы просто продолжаете цикл TDD, добавляя тесты и функциональные возможности, пока парсер команды не сделает все, что вам нужно.
В качестве упражнения подумайте, что есть еще одна команда, которая позволяет вам установить уровень громкости. Может быть, «v», за которым следует число. Как бы вы написали для этого тест? Это также приведет к появлению новых ошибок. Что, если номер недействителен? Что делать, если вы получаете «v», за которым сразу следует «m?»? Вы можете увидеть, как это может быстро усложниться. Но вы можете написать тест для каждого из этих случаев ошибок! В каждом случае вы точно знаете, как должен себя вести парсер команд - если он не будет проверять модульные тесты, вы узнаете.
Сократить сложные проблемы до более простых
Создание парсера команд (или любого программного модуля, если на то пошло) является сложной задачей. Если вы пытаетесь представить готовый модуль до того, как вы его начнете, это может быть сложно. Это особенно верно, если вы реализуете то, чего никогда не делали раньше, потому что у вас нет опыта применения каких-либо шаблонов проектирования. Краммирование всего этого в ваш мозг сразу приводит к высокой познавательной нагрузке.
Но подход, основанный на тестах, может снизить вашу когнитивную нагрузку, высвобождая ваш мозг, чтобы написать действительно отличное программное обеспечение. Посмотрите, что мы только что сделали в примере синтаксического анализа. На каждом шагу нас интересовал только следующий набор функциональных возможностей - не все возможности, которые мы могли бы добавить в будущем. Откладывая все эти другие проблемы до самого позднего времени, вы можете сосредоточить все внимание на одной вещи одновременно.
Инструменты
Так что TDD велик, не так ли? Одна из трудных проблем заключается в том, что встроенные средства тестирования (то есть C) не были такими замечательными. Когда вы делаете TDD, вы все время создаете и запускаете тесты. Это означает, что вам нужно очень просто добавить новые тесты и запустить их. Если все это сложно, вы, вероятно, разочаруетесь и сдадитесь.
Однако становится все лучше. Такие инструменты, как Ceedling (с Unity и CMock), могут быстро и быстро запускать вас. Ceedling обеспечивает автоматическое обнаружение тестов, разработку макета и выполнение теста, чтобы сделать вашу жизнь проще. Вот почему я рекомендую его кому-либо новому для встроенного модульного тестирования, и я написал конкретно о том, как использовать Ceedling для начала работы с TDD в C.
TDD широко не используется во встроенном программном обеспечении. Если вы начнете экспериментировать с TDD, вы будете настаивать на разработке встроенного программного обеспечения (и вы можете поговорить с друзьями и друзьями в веб-разработчике!). Там все еще будет много, чтобы учиться, но вы будете на пути к совершенствованию себя и своего кода. Я думаю, вам понравится то, что вы обнаружите.