Статья
Версия для печати
Обсудить на форуме
«Hello World!» в embedded-исполнении. Часть 4

© Dale, 06.11.2011 — 07.11.2011.


Начало: часть 1, часть 2, часть 3.

Оглавление


Подставной модуль Led

Итак, мы выяснили, что для тестирования модуля Application нам понадобятся подставные модули Led и Timer. Теоретически можно было бы написать их вручную, но этого не стоит делать по нескольким причинам. Во-первых, это рутинная работа, которую можно (и нужно) автоматизировать. Во-вторых, несмотря на рутинность, эта работа требует большого внимания и кропотливости, поскольку подставной объект — не такая уж простая штука. В-третьих, это сизифов труд, так как при каждом изменении интерфейса модуля подставной код нужно переписывать (или по крайней мере дописывать).
Поэтому сейчас самое время отправиться на домашнюю страницу проекта CMock и загрузить этот продукт в поддиректорию vendor\cmock нашего проекта. Мы будем далее работать с ним довольно много, поэтому не поленитесь отложить на время чтение этой статьи и внимательно прочитать документацию на CMock, тем более что ее совсем немного.
Прочитали? Продолжаем. Сначала создадим поддиректорию для модуля Led:

> CreateModule.bat Led

Затем сгенерируем заготовку модуля:

> GenMod.bat Led Led

Объявляем интерфейс модуля:

Код: (C) Led.c
#ifndef _LED_H
#define _LED_H

void Led_init(void);

void Led_on(void);

void Led_off(void);

#endif // _LED_H

Создадим в поддиректории модуля Led поддиректорию mock для хранения подставных объектов. А теперь пора применить новые знания, полученные в процессе изучения документации CMock:

> ruby ..\vendor\cmock\lib\cmock.rb include\Led.h

Сообщение на консоли уведомляет нас о том, что подставной объект для модуля Led был благополучно сгенерирован. Но раз уж мы с самого начала привыкли проверять каждую деталь лично, отправимся в только что созданную нами поддиректорию mocks и поглядим, что там произошло. Видим, что там появились два файла: MockLed.h и MockLed.c. С первым из них мы будем непосредственно работать далее, поэтому рассмотрим его подробно:

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

#include "Led.h"

void MockLed_Init(void);
void MockLed_Destroy(void);
void MockLed_Verify(void);




#define Led_init_Expect() Led_init_CMockExpect(__LINE__)
void Led_init_CMockExpect(UNITY_LINE_TYPE cmock_line);
#define Led_on_Expect() Led_on_CMockExpect(__LINE__)
void Led_on_CMockExpect(UNITY_LINE_TYPE cmock_line);
#define Led_off_Expect() Led_off_CMockExpect(__LINE__)
void Led_off_CMockExpect(UNITY_LINE_TYPE cmock_line);

#endif

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

  • Led_init_Expect();
  • Led_on_Expect();
  • Led_off_Expect().

Эти макросы, написанные в функциональном стиле, очень напоминают наши интерфейсные методы, к которым дописан суффикс _Expect. С их помощью производится настройка подставного объекта, т.е. мы сообщаем ему, какие методы, в каком порядке и с какими параметрами (если есть) должны быть вызваны в подставном объекте, а также какие значения (если есть) они должны вернуть в тестируемую программу. У нас самый простой случай — методы без параметров не возвращают значений, поэтому и сигнатура настроечного макроса столь же проста.
Модуль MockLed.c мы рассматривать не будем, поскольку он чисто служебный. Можете посмотреть его самостоятельно, чтобы оценить, от какого количества писанины избавил нас автогенератор CMock.
На этом мы можем временно распрощаться с модулем Led и вернуться к модулю Application, поскольку у нас теперь есть все необходимое, чтобы начать его реализацию посредством TDD.

Начинаем реализацию Application

Подготовка

К этому моменту среда Unity уже любезно сгенерировала для нас заготовку набора модульных тестов для Application (см. Часть 3 статьи). Сгенерируем к нему «несущую» программу (если забыли, для чего и как это делается, возвращаемся к статьям, посвященным среде Unity):

> ruby vendor\unity\auto\generate_test_runner.rb Application/test/TestApplication.c

Обратите внимание на то, что в спецификации набора тестов используются прямые слэши. Это сделано потому, что эта строка без изменений попадает в программу запуска тестового набора, и привычные для файловой системы MS Windows обратные слеши будут интерпретированы как экранирующие для последующих литер, что приведет к небольшим недоразумениям как при компиляции, так и при отображении строки. В поддиректории test появляется файл TestApplication_Runner.c:

Код: (C) TestApplication_Runner.c
/* AUTOGENERATED FILE. DO NOT EDIT. */

//=======Test Runner Used To Run Each Test Below=====
#define RUN_TEST(TestFunc, TestLineNum) \
{ \
  Unity.CurrentTestName = #TestFunc; \
  Unity.CurrentTestLineNumber = TestLineNum; \
  Unity.NumberOfTests++; \
  if (TEST_PROTECT()) \
  { \
      setUp(); \
      TestFunc(); \
  } \
  if (TEST_PROTECT() && !TEST_IS_IGNORED) \
  { \
    tearDown(); \
  } \
  UnityConcludeTest(); \
}


//=======Automagically Detected Files To Include=====
#include "unity.h"
#include <setjmp.h>
#include <stdio.h>

//=======External Functions This Runner Calls=====
extern void setUp(void);
extern void tearDown(void);
extern void test_Application_NeedToImplement(void);


//=======Test Reset Option=====
void resetTest()
{
  tearDown();
  setUp();
}


//=======MAIN=====
int main(void)
{
  Unity.TestFile = "Application/test/TestApplication.c";
  UnityBegin();
  RUN_TEST(test_Application_NeedToImplement, 12);

  return (UnityEnd());
}

Для компиляции набора тестов нам потребуется модифицировать make-файл:

Код: (Text) "Makefile"
# Подставьте здесь команду для вызова вашего компилятора C
CC=mingw32-gcc

CPPFLAGS=-I include

.DEFAULT : obj\PC\Application.o
obj\PC\Application.o : src\Application.c include\Application.h
        $(COMPILE.c) -o obj\PC\Application.o src\Application.c

.PHONY : clean
clean :
        del /q obj\PC\*.o
       

TestApplication_Runner.exe : obj\PC\TestApplication_Runner.o obj\PC\TestApplication.o ..\Blinker\obj\PC\unity.o
        $(LINK.c) -o TestApplication_Runner.exe obj\PC\TestApplication_Runner.o obj\PC\TestApplication.o \
..\Blinker\obj\PC\unity.o ..\Blinker\obj\PC\cmock.o

obj\PC\TestApplication_Runner.o : test\TestApplication_Runner.c
        $(COMPILE.c) -o obj\PC\TestApplication_Runner.o -I ..\vendor\unity\src test\TestApplication_Runner.c

obj\PC\TestApplication.o : test\TestApplication.c
        $(COMPILE.c) -o obj\PC\TestApplication.o -I ..\vendor\unity\src test\TestApplication.c

..\Blinker\obj\PC\unity.o : ..\vendor\unity\src\unity.c
        $(COMPILE.c) -o ..\Blinker\obj\PC\unity.o -I ..\vendor\unity\src ..\vendor\unity\src\unity.c

..\Blinker\obj\PC\cmock.o : ..\vendor\cmock\src\cmock.c
        $(COMPILE.c) -o ..\Blinker\obj\PC\cmock.o -I ..\vendor\cmock\src -I ..\vendor\unity\src \
..\vendor\cmock\src\cmock.c

Да, я полностью с вами согласен, выглядит он действительно далеко не лучшим образом; впрочем, не будем распылять наше внимание, всему свое время. Скоро доберемся и до него и приведем его в порядок. А сейчас воспользуемся результатом и запустим, наконец, наш первый набор тестов TestApplication_Runner.exe:

Application/test/TestApplication.c:14:test_Application_NeedToImplement:IGNORE
-----------------------
1 Tests 0 Failures 1 Ignored
OK

Результат полностью соответствует ожиданиям. Теперь можно написать настоящий тест.

Тест

Выглядит наш первый тест таким образом:

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

void setUp(void)
{
}

void tearDown(void)
{
}

void test_Application_init()
{
    Led_init_Expect();
       
    Application_init();
}

Тест test_Application_init() — это наш первый опыт применения подставного объекта для тестирования. В фазе настройки теста посредством вызова макроса Led_init_Expect() мы сообщаем тестовой среде, что ожидаем от тестируемого метода инициализации порта светодиода (посредством вызова метода Led_nit() либо непосредственно, либо косвенно). Затем мы вызываем тестируемый метод. Все, ничего больше не требуется. Тестовая среда сама обеспечит проверку того, что вызов метода инициализации действительно имел место, и выдаст ошибку, если это не так. Разумеется, самого метода инициализации пока что нет и в помине, мы еще не приступали к его реализации. Вместо настоящего метода тест вызывает подделку с тем же именем из поддельного модуля, сгенерированного CMock. Разумеется, тестируемый модуль не в состоянии обнаружить подделку, поскольку она реализует интерфейс «настоящего» модуля Led. Следовательно, поведение тестируемого модуля при тестировании ничем не отличается от поведения при реальной работе, а значит, результатам такого теста можно доверять.
Все это замечательно, но в процессе построения теста возникают неожиданные трудности:

obj\PC\TestApplication_Runner.o:TestApplication_Runner.c:(.text+0x65): undefined reference to `test_Application_NeedToImplement'
obj\PC\TestApplication.o:TestApplication.c:(.text+0x18): undefined reference to `Led_init_CMockExpect'
obj\PC\TestApplication.o:TestApplication.c:(.text+0x1d): undefined reference to `Application_init'
collect2: ld returned 1 exit status
make: *** [TestApplication_Runner.exe] Error 1

Компоновщик жалуется на то, что не может разрешить ссылку на test_Application_NeedToImplement. Но мы ведь собственноручно убрали этот автоматически сгенерированный тест из нашего набора, заменив его собственноручно написанным test_Application_init!
Увы, вызов test_Application_NeedToImplement остался в TestApplication_Runner.c. Мы позабыли о необходимости регенерировать программу запуска тестового набора каждый раз, когда изменяется наш тестовый набор TestApplication.c. Эту задачу мы адресуем утилите make. Нужно лишь прописать соответствующую зависимость в make-файле, который принимает такой вид:

Код: (Text) "Makefile"
# Подставьте здесь команду для вызова вашего компилятора C
CC=mingw32-gcc

CPPFLAGS=-I include -I ..\Led\include

.DEFAULT : obj\PC\Application.o
obj\PC\Application.o : src\Application.c include\Application.h ..\Led\include\Led.h
        $(COMPILE.c) -o obj\PC\Application.o src\Application.c

.PHONY : clean
clean :
        del /q obj\PC\*.o
       

TestApplication_Runner.exe : obj\PC\TestApplication_Runner.o obj\PC\TestApplication.o obj\PC\Application.o \
..\Blinker\obj\PC\unity.o ..\Blinker\obj\PC\cmock.o ..\Led\obj\PC\MockLed.o
        $(LINK.c) -o TestApplication_Runner.exe obj\PC\TestApplication_Runner.o obj\PC\TestApplication.o \
obj\PC\Application.o ..\Blinker\obj\PC\unity.o ..\Blinker\obj\PC\cmock.o ..\Led\obj\PC\MockLed.o

obj\PC\TestApplication_Runner.o : test\TestApplication_Runner.c
        $(COMPILE.c) -o obj\PC\TestApplication_Runner.o -I ..\vendor\unity\src -I ..\vendor\cmock\src \
-I ..\Led\mocks -I ..\Led\include test\TestApplication_Runner.c

obj\PC\TestApplication.o : test\TestApplication.c
        $(COMPILE.c) -o obj\PC\TestApplication.o -I ..\vendor\unity\src -I ..\Led\mocks -I ..\Led\include \
test\TestApplication.c

..\Blinker\obj\PC\unity.o : ..\vendor\unity\src\unity.c
        $(COMPILE.c) -o ..\Blinker\obj\PC\unity.o -I ..\vendor\unity\src ..\vendor\unity\src\unity.c

..\Blinker\obj\PC\cmock.o : ..\vendor\cmock\src\cmock.c
        $(COMPILE.c) -o ..\Blinker\obj\PC\cmock.o -I ..\vendor\cmock\src -I ..\vendor\unity\src \
..\vendor\cmock\src\cmock.c

..\Led\obj\PC\MockLed.o : ..\Led\mocks\MockLed.c
        $(COMPILE.c) -o ..\Led\obj\PC\MockLed.o -I ..\vendor\unity\src -I ..\vendor\cmock\src \
-I ..\Led\include ..\Led\mocks\MockLed.c

test\TestApplication_Runner.c : test\TestApplication.c
        ruby ..\vendor\unity\auto\generate_test_runner.rb test/TestApplication.c

Теперь по команде make TestApplication_Runner.exe наш тестовый набор успешно собран, и мы можем его запустить:

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

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

Код

Теперь наша задача — написать такой код в методе Application_init(), который позволит пройти всем имеющимся в данный момент модульным тестам (то есть единственному TestApplication.c), причем этот код должен быть минимальным. Поскольку ошибка теста вызвана тем, что ожидаемый метод Led_init() не был вызван, самый очевидный и простой способ ее исправить — это добавить упомянутый вызов:

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

void Application_init()
{
    Led_init();
}

void Application_run(void)
{
}

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

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

Прогресс налицо. Следующим шагом мы добавим в тест проверку инициализации таймера, чтобы наш тест мог полностью проверить Application_init() на соответствие спецификации.

Модуль Timer

Поскольку действия с модулем Timer принципиально ничем не отличаются от действий с модулем Led, я опишу их более кратко.
Ранее мы автоматизировали создание поддиректорий модуля при помощи файла сценария CreateModule.bat. После создания всех необходимых поддиректорий мы генерировали заготовку для модуля средствами Unity. Поскольку эти действия приходится выполнять совместно, имеет смысл расширить фунциональность CreateModule.bat:

Код: (DOS) CreateModule.bat
:: создание нового модуля
:: параметры:
::    %1 - имя модуля
::    %2 - необязательные параметры генератора модулей Unity

:: директория для модуля

mkdir %1

:: поддиректория для интерфейса

mkdir %1\include

:: поддиректория для исходных текстов

mkdir %1\src

:: поддиректория для объектных файлов инструментальной системы

mkdir %1\obj\PC

:: поддиректория для тестов

mkdir %1\test

:: поддиректория для подставных объектов

mkdir %1\mocks

:: генерация заготовки модуля

ruby vendor\unity\auto\generate_module.rb -i"%1/include" -s"%1/src" -t"%1/test" %1 %2
 

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

> CreateModule.bat Timer

E:\Projects\Shelek\Blinker\Software>mkdir Timer

E:\Projects\Shelek\Blinker\Software>mkdir Timer\include

E:\Projects\Shelek\Blinker\Software>mkdir Timer\src

E:\Projects\Shelek\Blinker\Software>mkdir Timer\obj\PC

E:\Projects\Shelek\Blinker\Software>mkdir Timer\test

E:\Projects\Shelek\Blinker\Software>mkdir Timer\mocks

E:\Projects\Shelek\Blinker\Software>ruby vendor\unity\auto\generate_module.rb -i"Timer/include" -s"Timer/src" -t"Timer/test" Timer  
File Timer/src/Timer.c created
File Timer/include/Timer.h created
File Timer/test/TestTimer.c created
Generate Complete

Заполняем интерфейс согласно спецификации модуля:

Код: (C) Timer.h
#ifndef _TIMER_H
#define _TIMER_H

#include <stdint.h>

void Timer_init(void);

void Timer_wait(uint16_t ms);

#endif // _TIMER_H

Поскольку наша программа предназначена для выполнения как минимум на двух различных архитектурах с разной разрядночтью — 32(64)-разрядной инструментальной IBM PC и 8-разрядной целевой Atmel AVR Mega (и при необходимости может быть легко портирована на другие архитектуры), то в целях переносимости мы не имеем права делать никаких предположений относительно разрядности стандартных целочисленных типов реализации языка C для данной платформы; вместо этого воспользуемся библиотечным типом uint16_t, который гарантированно является беззнаковым 16-разрядным целым на любой платформе. Поскольку я использую GCC, то могу воспользоваться типами, определенными стандартом C99 в заголовке stdint.h; с другими компиляторами, возможно, придется воспользоваться иными средствами для той же цели.
Далее по плану генерация подставного объекта для таймера. Поскольку мы делаем это действие уже второй раз и отныне будем делать постоянно, имеет смысл написать для этого сценарий:

Код: (DOS) GenMock.bat
cd %1
ruby ..\vendor\cmock\lib\cmock.rb include\%1.h

Сразу же его задействуем:

> GenMock.bat Timer


E:\Projects\Shelek\Blinker\Software>cd Timer

E:\Projects\Shelek\Blinker\Software\Timer>ruby ..\vendor\cmock\lib\cmock.rb include\Timer.h
Creating mock for Timer...

Получили подставной объект для модуля таймера:

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

#include "Timer.h"

void MockTimer_Init(void);
void MockTimer_Destroy(void);
void MockTimer_Verify(void);




#define Timer_init_Expect() Timer_init_CMockExpect(__LINE__)
void Timer_init_CMockExpect(UNITY_LINE_TYPE cmock_line);
#define Timer_wait_Expect(ms) Timer_wait_CMockExpect(__LINE__, ms)
void Timer_wait_CMockExpect(UNITY_LINE_TYPE cmock_line, uint16_t ms);

#endif

Обратите внимание на сигнатуру функционального макроса Timer_wait_Expect(ms). Поскольку его прототип в интерфейсе модуля имеет параметр, макрос при вызове задает параметр такого же типа. Это позволяет в фазе настройки указать не только факт, что метод Timer_wait должен быть вызван, но и с каким конкретно параметром ожидается этот вызов. Это наглядный пример опосредованного вывода, о котором шла речь в теоретической части статьи.

Завершаем реализацию Application

Теперь, когда у нас есть подставные объекты для обоих модулей, сервисами которых пользуется модуль Application, мы можем завершить реализацию этого модуля, причем он будет полностью протестирован. Сначала закончим с методом init():

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

void setUp(void)
{
}

void tearDown(void)
{
}

void test_Application_init()
{
    Led_init_Expect();
    Timer_init_Expect();
   
    Application_init();
}

Не буду приводить здесь изменения, которые необходимо внести в make-файл для включения нового подставного объекта в тестовую программу, поскольку они весьма просты, и читатель справится с ними самостоятельно. После запуска теста получаем:

> TestApplication_Runner.exe
test/TestApplication.c:14:test_Application_init:FAIL: Function 'Timer_init' called less times than expected.
-----------------------
1 Tests 1 Failures 0 Ignored
FAIL

Приводим в порядок init():

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

void Application_init(void)
{
    Led_init();
    Timer_init();
}

void Application_run(void)
{
}

Убеждаемся, что тест проходит успешно:

> TestApplication_Runner.exe
test/TestApplication.c:14:test_Application_init:PASS
-----------------------
1 Tests 0 Failures 0 Ignored
OK

Возвращаемся к спецификации метода Application.run() и составляем тест для него:

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

void setUp(void)
{
}

void tearDown(void)
{
}

void test_Application_init()
{
    Led_init_Expect();
    Timer_init_Expect();
   
    Application_init();
}

void test_Application_run()
{
    Led_on_Expect();
    Timer_wait_Expect(500);
    Led_off_Expect();
    Timer_wait_Expect(1500);
   
    Application_run();
}

Наши тесты полностью соответствуют принципу гибких технологий «тест как документация». Действительно, даже не располагая комплектом построенных нами ранее диаграмм UML, по одному лишь коду теста можно понять, что должен делать метод run(): включить светодиод, выждать 500 миллисекунд, выключить светодиод, выждать 1500 миллисекунд. Тест не только проверяет модуль на соответствие спецификации, но и одновременно является спецификацией.

Проверяем обязательное наличие ошибки при первом запуске:

> TestApplication_Runner.exe
test/TestApplication.c:14:test_Application_init:PASS
test/TestApplication.c:22:test_Application_run:FAIL: Function 'Led_on' called less times than expected.
-----------------------
2 Tests 1 Failures 0 Ignored
FAIL

Теперь последовательно устраняем ошибки, добавляя текст маленькими порциями. Получаем:

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

void Application_init(void)
{
    Led_init();
    Timer_init();
}

void Application_run(void)
{
    Led_on();
    Led_off();
    Timer_wait(500);
    Timer_wait(1500);
}

> TestApplication_Runner.exe
test/TestApplication.c:14:test_Application_init:PASS
test/TestApplication.c:22:test_Application_run:PASS
-----------------------
2 Tests 0 Failures 0 Ignored
OK

Неприятный сюрприз! Метод Application.run() явно не соответствует ожиданиям. В нашей реализации происходит следующее: светодиод включается, тут же выключается, после чего следуют подряд две паузы — на 500 и 1500 миллисекунд. Вряд ли самый зоркий глаз успеет заметить вспышку длительностью доли микросекунды взамен обещанной полусекунды. Мы имеем вроде бы правильный тест и код, который проходит этот тест без ошибок, являясь в то же время совершенно очевидно неработоспособным. Тупик.
Каждому инженеру знакомо правило: если ничего больше не помогает — изучи документацию. Обращаясь к описанию CMock, обнаруживаем, что продукт имеет массу параметров настройки, причем один из них по имени :enforce_strict_ordering как раз отвечает за проверку строгой упорядоченности вызовов. Я проверил содержимое файла cmock\lib\cmock_config.rb и обнаружил, что этот параметр имеет значение по умолчанию false. Параметрами настройки можно управлять, поместив их в конфигурационный файл в формате YAML и указав спецификацию этого файла как аргумент командной строки; при этом необходимо также задать этот параметр и в процессе генерации программы запуска набора тестов, иначе при ее компоновке возникнут ошибки. Поэтому в поддиректорию Softwareбыл добавлен конфигурационный файл enforce_strict_ordering.yml:

Код: (Text) enforce_strict_ordering.yml
---
:unity:
  :enforce_strict_ordering: 1

:cmock:
  :enforce_strict_ordering: 1

После перегенерации подставных модулей и программы запуска тестового набора тест выдал более адекватный результат:

> TestApplication_Runner.exe
Application/test/TestApplication.c:14:test_Application_init:PASS
Application/test/TestApplication.c:26:test_Application_run:FAIL: Function 'Led_off' called earlier than expected.
-----------------------
2 Tests 1 Failures 0 Ignored
FAIL

Корректируем метод с учетом обнаруженной ошибки:

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

void Application_init(void)
{
    Led_init();
    Timer_init();
}

void Application_run(void)
{
    Led_on();
    Timer_wait(500);
    Led_off();
    Timer_wait(1500);
}

Результат:

> TestApplication_Runner.exe
Application/test/TestApplication.c:14:test_Application_init:PASS
Application/test/TestApplication.c:22:test_Application_run:PASS
-----------------------
2 Tests 0 Failures 0 Ignored
OK

Вот теперь мы получили полностью протестированный модуль Application. Написанные нами тесты проверяют соответствие поведения модуля его спецификации.
Гарантирует ли это правильность программы в целом? Никоим образом. Наш модуль обращается к услугам модулей более низкого уровня, и если эти модули окажутся реализованы некорректно, программа не будет работать, хотя наш тест не сможет обнаружить ошибку. Имеет ли тогда смысл тратить столько сил и времени на тестирование, которое вроде бы не дает никаких гарантий?
Безусловно, имеет. Прежде всего, нужно понимать, что модульный тест проверяет лишь свой собственный модуль и не лезет в дела других модулей. Подразумевается, что прочие модули работают корректно. Кроме того, у этих прочих модулей должны быть свои собственные тесты, проверяющие правильность их работы, и они должны дать сигнал тревоги, если что-то в этих модулях пойдет не так.
Тесты, которые мы писали в этой статье, мало похожи на те, которые нам уже доводилось писать ранее. Наш модуль Application имеет высший уровень в иерархии модулей приложения, поэтому он сам не выполняет никакой конкретной деятельности, а лишь выдает поручения подчиненным модулям и координирует их работу (не правда ли, очень похоже на отношения между людьми?). Тесты такого рода, с которыми мы столкнулись на этот раз, относятся к категории интеграционных, которые проверяют правильность взаимодействия между модулями.
Тестирование не исчерпывается модульными тестами. Чтобы быть более-менее уверенными в работоспособности изделия, нужно подвергнуть его функциональному, а затем приемочному тестированию. Но об этом, возможно, мы поговорим позже.
Текущее состояние проекта вы, как обычно, можете найти в приложении.



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