Статья
Версия для печати
Обсудить на форуме
Многозадачность во встроенном приложении

© Dale, 05.12.2011 — 16.12.2011.

Начало: часть 1.

Часть 2

Оглавление


Каркас приложения

Построение каркаса приложения для микроконтроллера было подробно рассмотрено в статье «“Hello World!” в embedded-исполнении», поэтому здесь я не буду вдаваться в детали. Замечу лишь, что в данном проекте мы избежим возни с make-файлами, созданными вручную (это имело смысл сделать один раз, чтобы увидеть «изнанку» сборки проекта, но потребовало массу усилий, которые мы теперь будем экономить). На инструментальной системе я буду использовать среду разработки CodeBlocks (http://www.codeblocks.org).

Главная функция

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

Код: (C) Beacon.c
#include "Application.h"

int main()
{
    Application_init();

    for (;;)
        Application_run();
}



Код: (C) Application.h
#ifndef _APPLICATION_H
#define _APPLICATION_H

/** @file
 * @brief Интерфейс модуля Application.
 */


/**
 * @brief Инициализация приложения.
 *
 * Должна выполняться перед первым вызовом Application_run().
 */

void Application_init(void);

/**
 * @brief Итерация главного цикла приложения.
 */

void Application_run(void);

#endif // _APPLICATION_H

Модуль Application

Рис.2. Модуль Application
На данном этапе единственная задача модуля Application (Рис. 2) — управление будущим конечным автоматом, который мы вскоре реализуем (в данный момент делать это еще рано, ведь наша разработка движется в направлении сверху-вниз, поэтому каждый модуль появляется на сцене только в тот момент, когда уже готов вышестоящий модуль-клиент, нуждающийся в его услугах).
Интерфейс автомата также будет иметь два основных метода. Первый (init()) приведет автомат в исходное состояние, при помощи второго (run()) модуль Application будет периодически «подталкивать» автомат, давая ему возможность оценить происшедшие события и при необходимости совершить переход в другое состояние согласно диаграмме. Для целей тестирования нам понадобится также метод getState(), при помощи которого мы можем запросить текущее состояние автомата; это даст нам возможность сравнить фактическое поведение автомата с желаемым.
Раз уж речь зашла о состояниях, определим тип RedChannelAuto_State для его описания.
Интерфейс автомата на языке C будет выглядеть таким образом:

Код: (C) RedChannelAuto.c
#ifndef _REDCHANNELAUTO_H
#define _REDCHANNELAUTO_H

/** @file
 * @brief Интерфейс модуля RedChannelAuto.
 */


/**
 * @brief Инициализация модуля.
 *
 * Должна выполняться перед первым вызовом RedChannelAuto_run().
 */

void RedChannelAuto_init(void);

/**
 * @brief Итерация цикла модуля.
 */

void RedChannelAuto_run(void);

/**
 * @brief Состояние автомата.
 */

typedef enum
{
    LIGHT_OFF,
    LIGHT_ON
} RedChannelAuto_State;

/**
 * @brief Возвращает текущее состояние автомата
 */

RedChannelAuto_State RedChannelAuto_getState(void);

#endif // _REDCHANNELAUTO_H

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

Подставной объект RedChannelAuto

Как мы уже выяснили, при разработке «сверху-вниз» невозможно протестировать модуль без применения тестовых двойников модулей нижележащих уровней, которые использует тестируемый модуль. Поэтому первым делом нам следует сгенерировать подставной объект, используя интерфейс RedChannelAuto.c. Если вы забыли, как это делается, шпаргалка находится здесь.
Результат генерации:

Код: (C) MockRedChannelAuto.h
/* AUTOGENERATED FILE. DO NOT EDIT. */
#ifndef _MOCKREDCHANNELAUTO_H
#define _MOCKREDCHANNELAUTO_H

#include "RedChannelAuto.h"

void MockRedChannelAuto_Init(void);
void MockRedChannelAuto_Destroy(void);
void MockRedChannelAuto_Verify(void);




#define RedChannelAuto_init_Expect() RedChannelAuto_init_CMockExpect(__LINE__)
void RedChannelAuto_init_CMockExpect(UNITY_LINE_TYPE cmock_line);
#define RedChannelAuto_run_Expect() RedChannelAuto_run_CMockExpect(__LINE__)
void RedChannelAuto_run_CMockExpect(UNITY_LINE_TYPE cmock_line);
#define RedChannelAuto_getState_ExpectAndReturn(cmock_retval) \
    RedChannelAuto_getState_CMockExpectAndReturn(__LINE__, cmock_retval)

void RedChannelAuto_getState_CMockExpectAndReturn(UNITY_LINE_TYPE \
    cmock_line, RedChannelAuto_State cmock_to_return);

#endif

Метод Application.init

Задача метода Application.init — инициализировать автомат, чтобы впоследствии им можно было пользоваться. Напишем тест, который проверяет, была ли вызвана инициализация на самом деле.

Код: (C) TestApplication.c
#include "unity.h"
#include "Application.h"
#include "MockRedChannelAuto.h"

void setUp(void)
{
}

void tearDown(void)
{
}

void test_Application_init(void)
{
    RedChannelAuto_init_Expect();

    Application_init();
}

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

Код: (C) Application.c
#include "Application.h"
#include "RedChannelAuto.h"

void Application_init(void)
{

}

Запускаем тест:

test/TestApplication.c:13:test_Application_init:FAIL: Function 'RedChannelAuto_init' called less times than expected.
-----------------------
1 Tests 1 Failures 0 Ignored
FAIL

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

Код: (C) Application.c
#include "Application.h"
#include "RedChannelAuto.h"

void Application_init(void)
{
    RedChannelAuto_init();
}

Проверяем:

test/TestApplication.c:13:test_Application_init:PASS
-----------------------
1 Tests 0 Failures 0 Ignored
OK

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


Метод Application.run

Действия с методом Application.run аналогичны тем, которые мы только что проделали с Application.init? поэтому не буду повторяться, а лишь приведу результаты:

Код: (C) TestApplication.c
#include "unity.h"
#include "Application.h"
#include "MockRedChannelAuto.h"

void setUp(void)
{
}

void tearDown(void)
{
}

void test_Application_init(void)
{
    RedChannelAuto_init_Expect();

    Application_init();
}

void test_Application_run(void)
{
    RedChannelAuto_run_Expect();

    Application_run();
}

Код: (C) Application.c
#include "Application.h"
#include "RedChannelAuto.h"

void Application_init(void)
{
    RedChannelAuto_init();
}

void Application_run(void)
{
 
}

test/TestApplication.c:13:test_Application_init:PASS
test/TestApplication.c:20:test_Application_run:FAIL: Function 'RedChannelAuto_run' called less times than expected.
-----------------------
2 Tests 1 Failures 0 Ignored
FAIL

Код: (C) Application.c
#include "Application.h"
#include "RedChannelAuto.h"

void Application_init(void)
{
    RedChannelAuto_init();
}

void Application_run(void)
{
    RedChannelAuto_run();
}

test/TestApplication.c:13:test_Application_init:PASS
test/TestApplication.c:20:test_Application_run:PASS
-----------------------
2 Tests 0 Failures 0 Ignored
OK

Теперь модуль Application протестирован и полностью удовлетворяет своим спецификациям. Конечно, реализации RedChannelAuto_init() и RedChannelAuto_run(), которых еще не существует, могут содержать ошибки, из-за чего программа в целом работать не будет, несмотря на пройденные тесты. Однако это ни в коей мере не снижает ценности проведенного нами только что тестирования.
Дело в том, что модуль верхнего уровня (Application) использует в своей работе модуль нижнего уровня (RedChannelAuto) в предположении, что он корректно выполняет свои функции (то есть полностью соответствует своим спецификациям). Степень этого соответствия определят тесты модуля (RedChannelAuto_init()), когда до них дойдет дело. Пока же тестируемый модуль полагается на идеальную реализацию в виде двойника, лишенную ошибок. Если, например, метод RedChannelAuto_init() на самом деле не выполнит свои обязанности и автомат после инициализации окажется в неправильном состоянии, это никак не вина метода Application_init(), который честно выполнил свою работу. Поэтому тесты с использованием двойников ничуть не уступают в качестве тестам с реальными модулями при условии, что реальные модули также тщательно тестируются.

Модуль RedChannelAuto

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

Метод RedChannelAuto.init

Задача этого метода — инициализация автомата. Прежде всего, в процессе инициализации необходимо подготовить к работе вспомогательные модули, которыми будет пользоваться RedChannelAuto. Нам понадобится модуль для работы с датчиком освещенности (назовем его DayLightSensor), абстрактным таймером без привязки к конкретному оборудованию (IntervalTimer) и оборудованием для включения/выключения красного прожектора (RedLightSwitch). Каждый из вспомогательных модулей нуждается в собственной процедуре инициализации, что необходимо учесть при определении их интерфейсов.
Интерфейсы вспомогательных модулей:

Код: (C) DayLightSensor.h
#ifndef _DAYLIGHTSENSOR_H
#define _DAYLIGHTSENSOR_H

/**
 * @brief Инициализация модуля DayLightSensor.
 *
 * Должна выполняться перед использованием методов модуля.
 */

void DayLightSensor_init(void);

#endif // _DAYLIGHTSENSOR_H

Код: (C) RedLightSwitch.h
#ifndef _REDLIGHTSWITCH_H
#define _REDLIGHTSWITCH_H

/**
 * @brief Инициализация модуля RedLightSwitch.
 *
 * Должна выполняться перед использованием методов модуля.
 */

void RedLightSwitch_init(void);

/**
 * @brief Выключение прожектора.
 */

void RedLightSwitch_off(void);

#endif // _REDLIGHTSWITCH_H

Код: (C) IntervalTimer.h
#ifndef _INTERVALTIMER_H
#define _INTERVALTIMER_H

/**
 * @brief Инициализация модуля IntervalTimer.
 *
 * Должна выполняться перед использованием методов модуля.
 */

void IntervalTimer_init(void);

#endif // _INTERVALTIMER_H

Тест:

Код: (C) TestRedChannelAuto.c
#include "unity.h"
#include "RedChannelAuto.h"
#include "MockDayLightSensor.h"
#include "MockRedLightSwitch.h"
#include "MockIntervalTimer.h"

void setUp(void)
{
}

void tearDown(void)
{
}

/*
    Тест инициализации модуля.
    Датчик дневного света должен быть инициализирован.
    Оборудование для управления красным прожектором должно быть инициализировано.
    Таймер должен быть инициализирован.
    Красный прожектор должен быть выключен.
    После инициализации автомат должен находиться в состоянии INACTIVE
*/

void test_RedChannelAuto_Init(void)
{
    DayLightSensor_init_Expect();
    RedLightSwitch_init_Expect();
    IntervalTimer_init_Expect();
    RedLightSwitch_off_Expect();

    RedChannelAuto_init();

    TEST_ASSERT_EQUAL_INT_MESSAGE(INACTIVE, RedChannelAuto_getState(),
                                  "state must be INACTIVE after initialization");
}

Как я уже говорил ранее, я опускаю сообщения об ошибках, выдаваемые при тестировании автоматически сгенерированных «пустышек», не заполненных кодом. Предоставляю читателям возможность самостоятельно убедиться, что первый запуск теста завершается ошибкой, и привожу сразу код, успешно прошедший тестирование:

Код: (C) RedChannelAuto.c
#include "RedChannelAuto.h"
#include "DayLightSensor.h"
#include "RedLightSwitch.h"
#include "IntervalTimer.h"

static RedChannelAuto_State state;

void RedChannelAuto_init(void)
{
    DayLightSensor_init();
    RedLightSwitch_init();
    IntervalTimer_init();

    RedLightSwitch_off();
}

RedChannelAuto_State RedChannelAuto_getState(void)
{
    return state;
}

void RedChannelAuto_run(void)
{
}


Метод RedChannelAuto.run

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

Сохранение состояния INACTIVE днем

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

Код:
/**
 * @brief Опрос датчика дневного света.
 *
 * @return Возвращает значение 1 днем и 0 - ночью.
 */
_Bool DayLightSensor_isDay(void);

Тест:

Код: (C) TestRedChannelAuto.c
/*
    Проверка, что автомат, находящийся в состоянии INACTIVE, сохраняет это состояние
    в течение дня.
*/

void test_RedChannelAuto_Keep_State_INACTIVE_During_Day(void)
{
    DayLightSensor_init_Expect();
    RedLightSwitch_init_Expect();
    IntervalTimer_init_Expect();
    RedLightSwitch_off_Expect();
    RedChannelAuto_init();
    DayLightSensor_isDay_ExpectAndReturn((_Bool)1);

    RedChannelAuto_run();

    TEST_ASSERT_EQUAL_INT_MESSAGE(INACTIVE, RedChannelAuto_getState(),
                                  "State must be INACTIVE during day");
}

Код:

Код: (C) RedChannelAuto.c
void RedChannelAuto_run(void)
{
    if (DayLightSensor_isDay())
        return;
}

Тестовый набор успешно пройден, но по ходу дела тестовый код приобрел запах «Дублирующийся код». Проведем рефакторинг.
Подходящий «дезодорант» для данного случая — рефакторинг «Извлечение метода». Причем у нас уже есть наиболее подходящий метод для размещения в нем кода инициализации модуля. Ранее я рассказывал, что в модуле для запуска тестового набора есть функции setUp() и tearDown(). Первая из них всегда выполняется до запуска каждого теста, и ее задача — подготовить общую среду для выполнения тестов. Вторая всегда выполняется после запуска каждого теста, ее задача — «подчистить» последствия выполнения теста, если таковые имеются, и при этом они не слишком желательны. Функция setUp() замечательно подходит для нашей цели, поскольку тестировать неинициализированный модуль не имеет смысла — его поведение не определено.
Тестовый набор после рефакторинга:

Код: (C) TestRedChannelAuto.c
#include "unity.h"
#include "RedChannelAuto.h"
#include "MockDayLightSensor.h"
#include "MockRedLightSwitch.h"
#include "MockIntervalTimer.h"

void setUp(void)
{
    DayLightSensor_init_Expect();
    RedLightSwitch_init_Expect();
    IntervalTimer_init_Expect();
    RedLightSwitch_off_Expect();

    RedChannelAuto_init();
}

void tearDown(void)
{
}

/*
    Тест инициализации модуля.
    Датчик дневного света должен быть инициализирован.
    Оборудование для управления красным прожектором должно быть инициализировано.
    Таймер должен быть инициализирован.
    Красный прожектор должен быть выключен.
    После инициализации автомат должен находиться в состоянии INACTIVE
*/

void test_RedChannelAuto_Init(void)
{
    TEST_ASSERT_EQUAL_INT_MESSAGE(INACTIVE, RedChannelAuto_getState(),
                                  "State must be INACTIVE after initialization");
}

/*
    Проверка, что автомат, находящийся в состоянии INACTIVE, сохраняет это состояние
    в течение дня.
*/

void test_RedChannelAuto_Keep_State_INACTIVE_During_Day(void)
{
    DayLightSensor_isDay_ExpectAndReturn((_Bool)1);

    RedChannelAuto_run();

    TEST_ASSERT_EQUAL_INT_MESSAGE(INACTIVE, RedChannelAuto_getState(),
                                  "State must be INACTIVE during day");
}


Переход из INACTIVE в LIGHT_OFF ночью

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

Код: (C) IntervalTimer.h
/**
 * @brief Запуск отсчета заданного интервала.
 *
 * @param[in] interval Величина интервала в миллисекундах.
 */

void IntervalTimer_start(uint16_t interval);

Тест:

Код: (C) TestRedChannelAuto.c
/*
    Проверка: автомат, находящийся в состоянии INACTIVE, при наступлении дня должен
    перейти в состояние LIGHT_OFF.
    При переходе должен погаснуть красный прожектор и запуститься таймер на отсчет
    интервала между вспышками (1500 мс).
*/

void test_RedChannelAuto_Trans_From_INACTIVE_To_LIGHT_OFF_At_Night(void)
{
    // после инициализации автомат в состоянии INACTIVE
    TEST_ASSERT_EQUAL_INT_MESSAGE(INACTIVE, RedChannelAuto_getState(),
                                  "State expected to be INACTIVE");
    DayLightSensor_isDay_ExpectAndReturn((_Bool)0); // наступила ночь
    RedLightSwitch_off_Expect();    // красный прожектор должен быть погашен
    IntervalTimer_start_Expect((uint16_t)1500); // должен запуститься таймер на 1500 мс

    RedChannelAuto_run();

    TEST_ASSERT_EQUAL_INT_MESSAGE(LIGHT_OFF, RedChannelAuto_getState(),
                                  "State expected to be LIGHT_OFF");
}

Код:

Код: (C) RedChannelAuto.c
void RedChannelAuto_run(void)
{
    if (DayLightSensor_isDay())
        return;

    // переход в состояние LIGHT_OFF
    RedLightSwitch_off();
    IntervalTimer_start((uint16_t)1500);
    state = LIGHT_OFF;
}


Сохранение состояния LIGHT_OFF ночью до истечения интервала паузы

Нам потребуется способ узнать, истек ли заданный таймеру интервал:

Код: (C) IntervalTimer.h
/**
 * @brief Проверка истечения интервала, запущенного вызовом IntervalTimer_start().
 *
 * @return Признак истечения интервала.
 */

_Bool IntervalTimer_isElapsed(void);

Тест:

Код: (C) TestRedChannelAuto.c
/*
    Проверка: автомат, находящийся в состоянии LIGHT_OFF ночью, сохраняет это состояние
    в течение длительности паузы между вспышками.
*/

void test_RedChannelAuto_Keep_State_LIGHT_OFF_At_Night_During_Pause(void)
{
    // после инициализации автомат в состоянии INACTIVE
    TEST_ASSERT_EQUAL_INT_MESSAGE(INACTIVE, RedChannelAuto_getState(),
                                  "State expected to be INACTIVE");
    DayLightSensor_isDay_ExpectAndReturn((_Bool)0); // наступила ночь
    RedLightSwitch_off_Expect();    // красный прожектор должен быть погашен
    IntervalTimer_start_Expect((uint16_t)1500); // должен запуститься таймер на 1500 мс
    RedChannelAuto_run();

    DayLightSensor_isDay_ExpectAndReturn((_Bool)0); // продолжается ночь
    IntervalTimer_isElapsed_ExpectAndReturn((_Bool)0);  // таймер продолжает отсчет

    RedChannelAuto_run();

    TEST_ASSERT_EQUAL_INT_MESSAGE(LIGHT_OFF, RedChannelAuto_getState(),
                                  "State still expected to be LIGHT_OFF");
}

Тест обнаруживает, что после инициализации тест не находится в ожидаемом состоянии INACTIVE:

test/TestRedChannelAuto.c:80:test_RedChannelAuto_Keep_State_LIGHT_OFF_At_Night_During_Pause:FAIL:
 Expected 0 Was 1. State expected to be INACTIVE

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

Код: (C) RedChannelAuto.c
void RedChannelAuto_init(void)
{
    DayLightSensor_init();
    RedLightSwitch_init();
    IntervalTimer_init();

    RedLightSwitch_off();
    state = INACTIVE;
}

Теперь тест стартует с правильного состояния, но по-прежнему завершается с ошибкой. Неудивительно, ведь до сих пор в методе RedChannelAuto_run() нет логики, которая управляла бы поведением метода в зависимости от текущего состояния автомата. Добавляем нужный код:

Код: (C) RedChannelAuto.c
void RedChannelAuto_run(void)
{
    switch (state)
    {
        case INACTIVE:
            if (DayLightSensor_isDay())
                return;

            // переход в состояние LIGHT_OFF
            RedLightSwitch_off();
            IntervalTimer_start((uint16_t)1500);
            state = LIGHT_OFF;
            return;

        case LIGHT_OFF:
            if (!DayLightSensor_isDay())
            {
                if (!IntervalTimer_isElapsed())
                    return;
            }
    }
}

Тесты проходят. Не мешало бы убрать повторяющийся код.

Код: (C) TestRedChannelAuto.c
/*
    Вспомогательная функция: переход из INACTIVE в LIGHT_OFF ночью.
    Предусловие: состояние = INACTIVE
    Постусловия: состояние = LIGHT_OFF, выключен красный прожектор,
    запущен таймер на 1500 мс.
*/

static void helper_Trans_From_INACTIVE_To_LIGHT_OFF_At_Night(void)
{
    // после инициализации автомат в состоянии INACTIVE
    TEST_ASSERT_EQUAL_INT_MESSAGE(INACTIVE, RedChannelAuto_getState(),
                                  "Initial state expected to be INACTIVE");
    DayLightSensor_isDay_ExpectAndReturn((_Bool)0); // наступила ночь
    RedLightSwitch_off_Expect();    // красный прожектор должен быть погашен
    IntervalTimer_start_Expect((uint16_t)1500); // должен запуститься таймер на 1500 мс

    RedChannelAuto_run();
}

/*
    Проверка: автомат, находящийся в состоянии INACTIVE, при наступлении дня должен
    перейти в состояние LIGHT_OFF.
    При переходе должен погаснуть красный прожектор и запуститься таймер на отсчет
    интервала между вспышками (1500 мс).
*/

void test_RedChannelAuto_Trans_From_INACTIVE_To_LIGHT_OFF_At_Night(void)
{
    // переводим автомат в состояние LIGHT_OFF
    helper_Trans_From_INACTIVE_To_LIGHT_OFF_At_Night();

    TEST_ASSERT_EQUAL_INT_MESSAGE(LIGHT_OFF, RedChannelAuto_getState(),
                                  "State expected to be LIGHT_OFF");
}

/*
    Проверка: автомат, находящийся в состоянии LIGHT_OFF ночью, сохраняет это состояние
    в течение длительности паузы между вспышками.
*/

void test_RedChannelAuto_Keep_State_LIGHT_OFF_At_Night_During_Pause(void)
{
    // переводим автомат в состояние LIGHT_OFF
    helper_Trans_From_INACTIVE_To_LIGHT_OFF_At_Night();
    TEST_ASSERT_EQUAL_INT_MESSAGE(LIGHT_OFF, RedChannelAuto_getState(),
                                  "Initial state expected to be LIGHT_OFF");
    DayLightSensor_isDay_ExpectAndReturn((_Bool)0); // продолжается ночь
    IntervalTimer_isElapsed_ExpectAndReturn((_Bool)0);  // таймер продолжает отсчет

    RedChannelAuto_run();

    TEST_ASSERT_EQUAL_INT_MESSAGE(LIGHT_OFF, RedChannelAuto_getState(),
                                  "State still expected to be LIGHT_OFF");
}


Переход из LIGHT_OFF в INACTIVE с наступлением дня

Тест:

Код: (C) TestRedChannelAuto.c
/*
    Проверка: автомат, находящийся в состоянии LIGHT_OFF, с наступлением дня должен
    перейти в состояние INACTIVE.
    При переходе должен погаснуть красный прожектор.
*/

void test_RedChannelAuto_Trans_From_LIGHT_OFF_To_INACTIVE_At_Day(void)
{
    // переводим автомат в состояние LIGHT_OFF
    helper_Trans_From_INACTIVE_To_LIGHT_OFF_At_Night();
    TEST_ASSERT_EQUAL_INT_MESSAGE(LIGHT_OFF, RedChannelAuto_getState(),
                                  "Initial state expected to be LIGHT_OFF");
    DayLightSensor_isDay_ExpectAndReturn((_Bool)1); // наступил день
    RedLightSwitch_off_Expect();    // красный прожектор должен быть погашен

    RedChannelAuto_run();

    TEST_ASSERT_EQUAL_INT_MESSAGE(INACTIVE, RedChannelAuto_getState(),
                                  "State expected to be INACTIVE");
}

Код:

Код: (C) RedChannelAuto.c
void RedChannelAuto_run(void)
{
    switch (state)
    {
        case INACTIVE:
            if (DayLightSensor_isDay())
                return;
            else
            {
                // переход в состояние LIGHT_OFF
                RedLightSwitch_off();
                IntervalTimer_start((uint16_t)1500);
                state = LIGHT_OFF;
                return;
            }

        case LIGHT_OFF:
            if (DayLightSensor_isDay())
            {
                // переход в состояние INACTIVE
                RedLightSwitch_off();
                state = INACTIVE;
            }
            else
            {
                if (IntervalTimer_isElapsed())
                {
                }
            }
            return;
    }
}



Переход из LIGHT_OFF в LIGHT_ON по истечении паузы

Если автомат находится в состоянии LIGHT_OFF и продолжается ночь, по истечении паузы между вспышками он должен перейти в состояние LIGHT_ON. При этом загорается красный прожектор, таймер запускается на отсчет длительности вспышки.
Тест:

Код: (C) TestRedChannelAuto.c
/*
    Проверка: автомат, находящийся в состоянии LIGHT_OFF ночью, по истечении паузы
    между вспышками должен перейти в состояние LIGHT_ON.
    При переходе должен включиться красный прожектор и запуститься таймер на отсчет
    длительности вспышки (500 мс).
*/

void test_RedChannelAuto_Trans_From_LIGHT_OFF_To_LIGHT_ON_At_Night(void)
{
    // переводим автомат в состояние LIGHT_OFF
    helper_Trans_From_INACTIVE_To_LIGHT_OFF_At_Night();
    TEST_ASSERT_EQUAL_INT_MESSAGE(LIGHT_OFF, RedChannelAuto_getState(),
                                  "Initial state expected to be LIGHT_OFF");
    DayLightSensor_isDay_ExpectAndReturn((_Bool)0); // продолжается ночь
    IntervalTimer_isElapsed_ExpectAndReturn((_Bool)1);  // таймер завершил отсчет паузы
    RedLightSwitch_off_Expect();    // красный прожектор должен быть включен
    IntervalTimer_start_Expect((uint16_t)500); // должен запуститься таймер на 500 мс

    RedChannelAuto_run();

    TEST_ASSERT_EQUAL_INT_MESSAGE(LIGHT_ON, RedChannelAuto_getState(),
                                  "State expected to be LIGHT_ON");
}

Код:

Код: (C) RedChannelAuto.c
void RedChannelAuto_run(void)
{
    switch (state)
    {
        case INACTIVE:
            if (DayLightSensor_isDay())
                return;
            else
            {
                // переход в состояние LIGHT_OFF
                RedLightSwitch_off();
                IntervalTimer_start((uint16_t)1500);
                state = LIGHT_OFF;
                return;
            }

        case LIGHT_OFF:
            if (DayLightSensor_isDay())
            {
                // переход в состояние INACTIVE
                RedLightSwitch_off();
                state = INACTIVE;
            }
            else
            {
                if (IntervalTimer_isElapsed())
                {
                    // переход в состояние LIGHT_ON
                    RedLightSwitch_on();
                    IntervalTimer_start((uint16_t)500);
                    state = LIGHT_ON;
                }
            }
            return;
    }
}


Состояние LIGHT_ON

Реализация состояния LIGHT_ON не имеет принципиальных отличий от LIGHT_OFF, поэтому не буду расписывать ее пошагово.


Итоги разработки модуля

Приведу конечный результат.
Тест:

Код: (C) TestRedChannelAuto.c
#include "unity.h"
#include "RedChannelAuto.h"
#include "MockDayLightSensor.h"
#include "MockRedLightSwitch.h"
#include "MockIntervalTimer.h"

void setUp(void)
{
    DayLightSensor_init_Expect();
    RedLightSwitch_init_Expect();
    IntervalTimer_init_Expect();
    RedLightSwitch_off_Expect();

    RedChannelAuto_init();
}

void tearDown(void)
{
}

/*
    Тест инициализации модуля.
    Датчик дневного света должен быть инициализирован.
    Оборудование для управления красным прожектором должно быть инициализировано.
    Таймер должен быть инициализирован.
    Красный прожектор должен быть выключен.
    После инициализации автомат должен находиться в состоянии INACTIVE
*/

void test_RedChannelAuto_Init(void)
{
    TEST_ASSERT_EQUAL_INT_MESSAGE(INACTIVE, RedChannelAuto_getState(),
                                  "State must be INACTIVE after initialization");
}

/*
    Проверка: автомат, находящийся в состоянии INACTIVE, сохраняет это состояние
    в течение дня.
*/

void test_RedChannelAuto_Keep_State_INACTIVE_During_Day(void)
{
    TEST_ASSERT_EQUAL_INT_MESSAGE(INACTIVE, RedChannelAuto_getState(),
                                  "Initial state expected to be INACTIVE");
    DayLightSensor_isDay_ExpectAndReturn((_Bool)1);

    RedChannelAuto_run();

    TEST_ASSERT_EQUAL_INT_MESSAGE(INACTIVE, RedChannelAuto_getState(),
                                  "State must be INACTIVE during day");
}

/*
    Вспомогательная функция: переход из INACTIVE в LIGHT_OFF ночью.
    Предусловие: состояние = INACTIVE
    Постусловия: состояние = LIGHT_OFF, выключен красный прожектор,
    запущен таймер на 1500 мс.
*/

static void helper_Trans_From_INACTIVE_To_LIGHT_OFF_At_Night(void)
{
    // после инициализации автомат в состоянии INACTIVE
    TEST_ASSERT_EQUAL_INT_MESSAGE(INACTIVE, RedChannelAuto_getState(),
                                  "Initial state expected to be INACTIVE");
    DayLightSensor_isDay_ExpectAndReturn((_Bool)0); // наступила ночь
    RedLightSwitch_off_Expect();    // красный прожектор должен быть погашен
    IntervalTimer_start_Expect((uint16_t)1500); // должен запуститься таймер на 1500 мс

    RedChannelAuto_run();
}

/*
    Проверка: автомат, находящийся в состоянии INACTIVE, при наступлении ночи должен
    перейти в состояние LIGHT_OFF.
    При переходе должен погаснуть красный прожектор и запуститься таймер на отсчет
    интервала между вспышками (1500 мс).
*/

void test_RedChannelAuto_Trans_From_INACTIVE_To_LIGHT_OFF_At_Night(void)
{
    // переводим автомат в состояние LIGHT_OFF
    helper_Trans_From_INACTIVE_To_LIGHT_OFF_At_Night();

    TEST_ASSERT_EQUAL_INT_MESSAGE(LIGHT_OFF, RedChannelAuto_getState(),
                                  "State expected to be LIGHT_OFF");
}

/*
    Проверка: автомат, находящийся в состоянии LIGHT_OFF ночью, сохраняет это состояние
    в течение длительности паузы между вспышками.
*/

void test_RedChannelAuto_Keep_State_LIGHT_OFF_At_Night_During_Pause(void)
{
    // переводим автомат в состояние LIGHT_OFF
    helper_Trans_From_INACTIVE_To_LIGHT_OFF_At_Night();
    TEST_ASSERT_EQUAL_INT_MESSAGE(LIGHT_OFF, RedChannelAuto_getState(),
                                  "Initial state expected to be LIGHT_OFF");
    DayLightSensor_isDay_ExpectAndReturn((_Bool)0); // продолжается ночь
    IntervalTimer_isElapsed_ExpectAndReturn((_Bool)0);  // таймер продолжает отсчет паузы

    RedChannelAuto_run();

    TEST_ASSERT_EQUAL_INT_MESSAGE(LIGHT_OFF, RedChannelAuto_getState(),
                                  "State still expected to be LIGHT_OFF");
}

/*
    Проверка: автомат, находящийся в состоянии LIGHT_OFF, с наступлением дня должен
    перейти в состояние INACTIVE.
    При переходе должен погаснуть красный прожектор.
*/

void test_RedChannelAuto_Trans_From_LIGHT_OFF_To_INACTIVE_At_Day(void)
{
    // переводим автомат в состояние LIGHT_OFF
    helper_Trans_From_INACTIVE_To_LIGHT_OFF_At_Night();
    TEST_ASSERT_EQUAL_INT_MESSAGE(LIGHT_OFF, RedChannelAuto_getState(),
                                  "Initial state expected to be LIGHT_OFF");
    DayLightSensor_isDay_ExpectAndReturn((_Bool)1); // наступил день
    RedLightSwitch_off_Expect();    // красный прожектор должен быть погашен

    RedChannelAuto_run();

    TEST_ASSERT_EQUAL_INT_MESSAGE(INACTIVE, RedChannelAuto_getState(),
                                  "State expected to be INACTIVE");
}

/*
    Вспомогательная функция: переход из INACTIVE в LIGHT_ON ночью.
    Предусловие: состояние = INACTIVE
    Постусловия: состояние = LIGHT_ON, включен красный прожектор,
    запущен таймер на 500 мс.
*/

static void helper_Trans_From_INACTIVE_To_LIGHT_ON_At_Night(void)
{
    // переводим автомат в состояние LIGHT_OFF
    helper_Trans_From_INACTIVE_To_LIGHT_OFF_At_Night();
    TEST_ASSERT_EQUAL_INT_MESSAGE(LIGHT_OFF, RedChannelAuto_getState(),
                                  "State expected to be LIGHT_OFF");
    DayLightSensor_isDay_ExpectAndReturn((_Bool)0); // продолжается ночь
    IntervalTimer_isElapsed_ExpectAndReturn((_Bool)1);  // таймер завершил отсчет паузы
    RedLightSwitch_on_Expect();    // красный прожектор должен быть включен
    IntervalTimer_start_Expect((uint16_t)500); // должен запуститься таймер на 500 мс

    RedChannelAuto_run();
}

/*
    Проверка: автомат, находящийся в состоянии LIGHT_OFF ночью, по истечении паузы
    между вспышками должен перейти в состояние LIGHT_ON.
    При переходе должен включиться красный прожектор и запуститься таймер на отсчет
    длительности вспышки (500 мс).
*/

void test_RedChannelAuto_Trans_From_LIGHT_OFF_To_LIGHT_ON_At_Night(void)
{
    helper_Trans_From_INACTIVE_To_LIGHT_ON_At_Night();

    TEST_ASSERT_EQUAL_INT_MESSAGE(LIGHT_ON, RedChannelAuto_getState(),
                                  "State expected to be LIGHT_ON");
}

/*
    Проверка: автомат, находящийся в состоянии LIGHT_ON ночью, сохраняет это состояние
    в течение длительности вспышки.
*/

void test_RedChannelAuto_Keep_State_LIGHT_ON_At_Night_During_Flash(void)
{
    // переводим автомат в состояние LIGHT_ON
    helper_Trans_From_INACTIVE_To_LIGHT_ON_At_Night();
    TEST_ASSERT_EQUAL_INT_MESSAGE(LIGHT_ON, RedChannelAuto_getState(),
                                  "Initial state expected to be LIGHT_ON");
    DayLightSensor_isDay_ExpectAndReturn((_Bool)0); // продолжается ночь
    IntervalTimer_isElapsed_ExpectAndReturn((_Bool)0);  // таймер продолжает отсчет вспышки

    RedChannelAuto_run();

    TEST_ASSERT_EQUAL_INT_MESSAGE(LIGHT_ON, RedChannelAuto_getState(),
                                  "State still expected to beLIGHT_ON");
}


/*
    Проверка: автомат, находящийся в состоянии LIGHT_ON, с наступлением дня должен
    перейти в состояние INACTIVE.
    При переходе должен погаснуть красный прожектор.
*/

void test_RedChannelAuto_Trans_From_LIGHT_ON_To_INACTIVE_At_Day(void)
{
    // переводим автомат в состояние LIGHT_ON
    helper_Trans_From_INACTIVE_To_LIGHT_ON_At_Night();
    TEST_ASSERT_EQUAL_INT_MESSAGE(LIGHT_ON, RedChannelAuto_getState(),
                                  "Initial state expected to be LIGHT_ON");
    DayLightSensor_isDay_ExpectAndReturn((_Bool)1); // наступил день
    RedLightSwitch_off_Expect();    // красный прожектор должен быть погашен

    RedChannelAuto_run();

    TEST_ASSERT_EQUAL_INT_MESSAGE(INACTIVE, RedChannelAuto_getState(),
                                  "State expected to be INACTIVE");
}

/*
    Проверка: автомат, находящийся в состоянии LIGHT_ON ночью, по истечении паузы
    между вспышками должен перейти в состояние LIGHT_OFF.
    При переходе должен выключиться красный прожектор и запуститься таймер на отсчет
    интервала между вспышками (1500 мс).
*/

void test_RedChannelAuto_Trans_From_LIGHT_ON_To_LIGHT_OFF_At_Night(void)
{
    // переводим автомат в состояние LIGHT_ON
    helper_Trans_From_INACTIVE_To_LIGHT_ON_At_Night();
    TEST_ASSERT_EQUAL_INT_MESSAGE(LIGHT_ON, RedChannelAuto_getState(),
                                  "Initial state expected to be LIGHT_ON");
    DayLightSensor_isDay_ExpectAndReturn((_Bool)0); // продолжается ночь
    IntervalTimer_isElapsed_ExpectAndReturn((_Bool)1);  // таймер завершил отсчет паузы
    RedLightSwitch_off_Expect();    // красный прожектор должен быть погашен
    IntervalTimer_start_Expect((uint16_t)1500); // должен запуститься таймер на 1500 мс

    RedChannelAuto_run();

    TEST_ASSERT_EQUAL_INT_MESSAGE(LIGHT_OFF, RedChannelAuto_getState(),
                                  "State expected to be LIGHT_OFF");
}

Код:

Код: (C) RedChannelAuto.c
#include "RedChannelAuto.h"
#include "DayLightSensor.h"
#include "RedLightSwitch.h"
#include "IntervalTimer.h"

static RedChannelAuto_State state;

void RedChannelAuto_init(void)
{
    DayLightSensor_init();
    RedLightSwitch_init();
    IntervalTimer_init();

    RedLightSwitch_off();
    state = INACTIVE;
}

RedChannelAuto_State RedChannelAuto_getState(void)
{
    return state;
}

// переход в состояние INACTIVE
static void transTo_INACTIVE(void)
{
    RedLightSwitch_off();
    state = INACTIVE;
}

// переход в состояние LIGHT_OFF
static void transTo_LIGHT_OFF(void)
{
    RedLightSwitch_off();
    IntervalTimer_start((uint16_t)1500);
    state = LIGHT_OFF;
}

// переход в состояние LIGHT_ON
static void transTo_LIGHT_ON(void)
{
    RedLightSwitch_on();
    IntervalTimer_start((uint16_t)500);
    state = LIGHT_ON;
}

void RedChannelAuto_run(void)
{
    switch (state)
    {
        case INACTIVE:
            if (DayLightSensor_isDay())
            {
            }
            else
            {
                transTo_LIGHT_OFF();
            }
            return;

        case LIGHT_OFF:
            if (DayLightSensor_isDay())
            {
                transTo_INACTIVE();
            }
            else
            {
                if (IntervalTimer_isElapsed())
                {
                    transTo_LIGHT_ON();
                }
            }
            return;

        case LIGHT_ON:
            if (DayLightSensor_isDay())
            {
                transTo_INACTIVE();
            }
            else
            {
                if (IntervalTimer_isElapsed())
                {
                    transTo_LIGHT_OFF();
                }
            }
            return;
    }
}

Результат тестирования:

test/TestRedChannelAuto.c:29:test_RedChannelAuto_Init:PASS
test/TestRedChannelAuto.c:39:test_RedChannelAuto_Keep_State_INACTIVE_During_Day:PASS
test/TestRedChannelAuto.c:75:test_RedChannelAuto_Trans_From_INACTIVE_To_LIGHT_OFF_At_Night:PASS
test/TestRedChannelAuto.c:88:test_RedChannelAuto_Keep_State_LIGHT_OFF_At_Night_During_Pause:PASS
test/TestRedChannelAuto.c:108:test_RedChannelAuto_Trans_From_LIGHT_OFF_To_INACTIVE_At_Day:PASS
test/TestRedChannelAuto.c:149:test_RedChannelAuto_Trans_From_LIGHT_OFF_To_LIGHT_ON_At_Night:PASS
test/TestRedChannelAuto.c:161:test_RedChannelAuto_Keep_State_LIGHT_ON_At_Night_During_Flash:PASS
test/TestRedChannelAuto.c:182:test_RedChannelAuto_Trans_From_LIGHT_ON_To_INACTIVE_At_Day:PASS
test/TestRedChannelAuto.c:203:test_RedChannelAuto_Trans_From_LIGHT_ON_To_LIGHT_OFF_At_Night:PASS
-----------------------
9 Tests 0 Failures 0 Ignored
OK

Структура проекта на данный момент представлена на рис. 3:
Рис. 3. Структура проекта.

Текущее состояние проекта находится в приложении.



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

Тестовый код
Код продукции
Кол-во строк
218
88

Конечно, это очень грубая метрика; я взял общее количество строк и не учитывал пустые строки, а также то, что в тестовом коде больше комментариев, чем в продукционном. Впрочем, написание комментариев тоже требует времени программиста, а написание информативных комментариев — еще и умственных усилий. В целом размер тестового файла получился примерно в 2.5 раза больше, чем размер файла с исходным кодом. Это может натолкнуть на мысль, что использование TDD делает проектирование дороже, а также приводит к затягиванию сроков. Многих программистов эта мысль так пугает, что они отвергают идею TDD, даже не попробовав на практике: им и так все кажется очевидным.
На самом деле поспешное суждение не всегда оказывается самым верным. Действительно, мы платим за TDD тем, что вынуждены набрать без малого вчетверо больше кода, чем без него. (Кстати, коэффициент 2.5 — это далеко не предел; в «больших» проектах приложений корпоративного уровня с повышенными требованиями к надежности у меня этот показатель нередко доходит до 5; впрочем, эти проекты не имеют отношения к миру firmware). Не переплачиваем ли мы, часом?
На мой взгляд — определенно нет. Все имеет свою цену, и в данном случае она никоим образом не завышена. Прежде всего посмотрим на тестовый код. Он очень прост. Прежде всего, он линеен и не имеет ветвлений. Тест, написанный по четырехфазной схеме, состоит лишь из нескольких строк предварительной подготовки, одной строки вызова тестируемой функции и нескольких строк оценки результата теста. Если мы посмотрим на продукционный код, например, метода RedChannelAuto_run(), увидим, что он гораздо сложнее; например, метрики цикломатической сложности для них просто несопоставимы. Кроме того, логика теста напрямую вытекает из спецификации требований; если аналитик на своем этапе потрудился на славу и требования хорошо структурированы, полны и понятны, то писать тестовый код можно с существенно большей скоростью, чем продукционный. Немалую помощь в данном случае оказывают макросы в стиле xUnit, которые образуют своего рода DSL (Domain Specific Language).
Итак, мы выяснили, что цена TDD в действительности не столь высока, как кажется со стороны. Что же мы получаем взамен?
  • Мы получили модуль, полностью прошедший все написанные для него тесты. Если набор тестов соответствует спецификациям, это автоматически означает, что и сам модуль соответствует тем же спецификациям. А это самый главный показатель качества продукта.
  • Не менее важно, что тестирование производится автоматически без каких-либо дополнительных трудозатрат, причем тесты выполняются очень быстро (вместе с полной сборкой набор тестов проходит меньше чем за минуту). Следовательно, впоследствии мы сможем повторять тестирование модуля так часто, как сочтем нужным, и это не скажется негативно на сроках разработки. Это дает разработчику больше свободы для модификации кода, не опасаясь, что побочными эффектами будет затронута ранее реализованная функциональность. Можно смело проводить рефакторинг, оптимизацию кода или добавление новых функций, зная, что после каждой итерации мы можем убедиться в соответствии кода спецификациям при помощи регрессионного тестирования. В случае провала тестов всегда можно произвести откат к рабочей версии.
  • Располагая заранее написанными тестами, при разработке кода мы не пользовались отладкой. В ней попросту не было необходимости, поскольку хорошо написанный тест дает полную информацию о возникшей ошибке, и ее достаточно просто обнаружить простым чтением подозрительного участка кода. Поскольку, вопреки ожиданиям оптимистов, отладка нетривиального кода занимает львиную долю всего цикла разработки, сокращение или даже полное исчезновение данного этапа полностью компенсирует затраты на написание тестов. (Это относится даже к «обычным», т.е. настольным и серверным системам; насколько может быть трудоемкой отладка встроенной системы, могут представить лишь те, кто реально ей занимался).
  • Как правило, плохо написанный код трудно тестировать. Необходимость тестирования попросту вынуждает повышать качество кода. Если тесты пишутся перед кодом, это приводит к результирующему коду более высокого качества, поскольку просто не позволяет продвигаться вперед, не приведя код в порядок.
  • Написание тестов после кода в принципе тоже возможно. Однако это существенно труднее (подробнее см. Майкл К. Физерс, «Эффективная работа с унаследованным кодом»). Альтернатива «не писать модульные тесты, полагаясь исключительно на ручное тестирование» вряд ли может восприниматься всерьез ввиду большой трудоемкости и низкой эффективности. Ну а вариант «не тестировать вообще, поскольку я гениален, и мой код тоже» профессионалу и подавно не годится.
  • Мы можем еще улучшить процесс групповой разработки, если установим строгое правило: в репозиторий проекта можно помещать лишь код, полностью прошедший все модульные тесты. При этом каждый разработчик начинает свою итерацию с рабочего кода и заканчивает им же. Естественно, это способствует повышению качества кода.
Если сопоставить расходы на TDD и полученные в результате выгоды, приходим к выводу, что баланс положителен и дополнительные усилия потрачены не зря.

Продолжение.
Версия для печати
Обсудить на форуме