Статья
Версия для печати
Обсудить на форуме (6)
Разработка на языке C, управляемая тестированием


© Dale, 25.07.2011 — 26.07.2011.




В прошлых статьях (части 1, 2, 3) мы освоили инструмент для модульного тестирования программ, написанных на языке C, под названием Unity. Однако обладание отдельным инструментом и даже уверенные навыки работы с ним еще не гарантируют достижения конечной цели. Нужно уметь вписать операции, которые способен выполнять данный инструмент, в общую технологию.
Само по себе модульное тестирование — нужная и важная часть процесса производства ПО. Однако наиболее полно раскрыть его потенциал позволяет технология, которая получила название «разработка, управляемая тестированием» (Test Driven Design, TDD), или в дальнейшем РУТ.
Оценить что-то новое проще в сравнении со старым, привычным. Поэтому сначала вспомним, как производилась разработка до появления РУТ.

Разработка, не управляемая тестированием

С названием для данного раздела у меня возникли совершенно непредвиденные затруднения.
Поначалу само собой напрашивалось название «Традиционные методы разработки», но тогда логически следует, что далее я буду описывать «нетрадиционные» методы, а это слово в последнее время приобрело какой-то двусмысленный оттенок, идет ли речь о медицине, сексуальной ориентации или чем-то еще. Вариант «общепринятые методы» немногим лучше, — ведь TDD получает все большее признание и уже не является экзотикой. Чтобы не тратить драгоценное время на подобные второстепенные вопросы, я решил остановиться на непритязательно-нейтральном «Разработка, не управляемая тестированием» (РНУТ).
Итак, что представляет собой РНУТ? На самом деле этот подход очень хорошо знаком всем без исключения разработчикам: код пишется, компилируется, а затем тестируется (как именно тестируется, в данный момент неважно; основной акцент делаем на последовательности действий, а именно: тестированию подвергается уже готовый, полностью написанный код).
Долгое время такая последовательность казалась естественной и единственно возможной. Ведь тестирование — это по сути проверка, а проверить можно лишь то, что уже существует.
Однако тестировать готовый код не всегда легко. Во-первых, не каждый код сам по себе пригоден для тестирования. Простой пример: некоторая функция (метод) получает извне (скажем, из базы данных) некоторые данные, а затем производит их обработку по сложному алгоритму с множеством ветвлений в зависимости от значений этих данных. Чтобы протестировать эту функцию, необходимо сначала заполнить базу данных тестовыми данными, поскольку в реальной БД может не оказаться необходимых для тестирования значений. Занесение таких данных в рабочую БД может оказать негативное влияние на рабочий процесс, если БД активно используется, а разворачивание специальной базы для тестирования и заполнение ее данными может оказаться достаточно трудоемким. В таких условиях отношение разработчиков к тестированию гарантированно будет негативным, даже если они и осознают реальную пользу от него. Понадобится глубокий рефакторинг кода, чтобы сделать такую функцию действительно пригодной для простого и эффективного тестирования.
Во-вторых, при тестировании готового кода зачастую бывает достаточно трудно добиться полного покрытия кода тестами; особенно хлопотно тестировать код обработки ошибок, которые трудно вызвать или имитировать. Пример: в нашей системе используется запись данных на SD-карты. Есть некоторый код, который обрабатывает ошибки в случае, если запись на карту невозможна из-за ее аппаратной неисправности (дефектные ячейки). Попробуйте-ка испортить исправную карту таким образом, чтобы она выдавала именно нужную нам для тестирования ошибку (либо сделать аппаратный имитатор сбойной карты). Поскольку все это сделать в принципе можно, но достаточно трудоемко, в итоге подобные ветки кода так и остаются не протестированными должным образом, образуя потенциальную мину замедленного действия, которая рванет, когда редкое условие все-таки сработает.
В-третьих, в программах нередко встречаются фрагменты незадействованного кода. Причины для этого разные. Иногда «запасливый» программист запасает функции впрок в полной уверенности, что они понадобятся в ближайшем будущем. Особенно часто такая ситуация встречается при проектировании и реализации «снизу-вверх», когда сначала пишутся вспомогательные модули, а затем основные, пользующиеся услугами вспомогательных. Некоторые из них оказываются вовсе невостребованными, другие и пригодились бы в принципе, но об их существовании уже забыли, к тому же написать их заново оказывается быстрее, чем найти в ворохе плохо оформленного кода. Иногда «мертвый» код появляется по объективным причинам, поскольку изменились требования к продукту и часть ранее реализованных функций теперь не нужна. Независимо от причин появления «мертвого» кода в программе, его наличие не способствует повышению качества продукта, поскольку распознать его не так просто, а сопровождать программу, изобилующую подобными шарадами и головоломками, становится труднее.Наконец, когда сроки сдачи проекта поджимают, слишком велик соблазн пожертвовать тестированием, сократив его объем, а то и проигнорировав вовсе, ведь клиент платит не за тесты. Поскольку планирование программных проектов — дисциплина весьма тонкая, вероятность того, что до полноценного тестирования дело не дойдет вовсе, достаточно велика.
Итак, при том, что РНУТ представляется довольно логичной и понятной, ей присущ ряд недостатков, которые затрудняют последующее тестирование и не лучшим образом влияют на качество продукта.

Разработка, управляемая тестированием

Разработка, управляемая тестированием (РУТ), как легко догадаться, представляет собой РНУТ, вывернутую наизнанку. При таком подходе сначала пишутся тесты и лишь потом сам код, который этим тестам удовлетворяет. РУТ поначалу удивляет и даже шокирует многих разработчиков; сама идея тестировать то, чего еще нет, кажется на первый взгляд абсурдной. Но это лишь на первый взгляд.
РУТ — одна из основ так называемых «гибких» (agile) технологий разработки, популярность которых в настоящее время стремительно возрастает («гибкими» они называются из-за способности приспосабливаться к динамичным, постоянно меняющимся требованиям к изделию). «Гибкие» технологии сами по себе весьма интересны и заслуживают отдельной статьи (и даже не одной), но сейчас мы не будем отходить от основной темы.
Порядок действий разработчика, который избрал для себя РУТ, примерно таков:

  • Найти в спецификациях требований к продукту такие, которые еще не были реализованы.

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

  • Реализовать очередной пункт требований в виде теста (набора тестов), который проверяет выполнение данного требования. Добавить новый тест к общему набору тестов.
  • Выполнить набор тестов. Убедиться, что его выполнение завершается с ошибкой. (Это естественно, поскольку для только что добавленных тестов из п. 2 еще не было написано никакого кода, реализующего проверяемые требования).
  • Написать необходимый минимум кода для прохождения набора тестов.
  • Выполнить набор тестов.
  • Повторять п.п. 4 и 5 до тех пор, пока не пройдут все тесты.
  • Проверить код и тесты на предмет «запахов», при необходимости произвести рефакторинг.
  • Выполнить набор тестов.
  • Повторять п.п. 7 и 8 до тех пор, пока не пройдут все тесты.
  • Вернуться к п. 1.

Такой подход не покажется столь странным, если изменить взгляд на саму задачу тестирования. Сформулируем эту задачу таким образом: определение фактических параметров предмета тестирования, сравнение их с желаемыми (перечисленными в спецификациях требований) и вынесения вердикта о соответствии (либо несоответствии) первых и вторых. Теперь написание тестов перед кодом уже не выглядит так странно, напротив: сначала мы определяемся с тем, что должны сделать, затем подготавливаем средства для проверки, что это будет работать, как задумано, и только потом реализуем задуманное.
В теории все выглядит гладко? Пришла пора убедиться в том, что и на практике оно работает ничуть не хуже. По традиции не будем искать легких путей и посмотрим, как РУТ работает в самой консервативной области программирования, которая обычно дольше всех сопротивляется введению всяческих новшеств, — в области разработки ПО на языке C для микроконтроллеров.
В качестве примера возьмем уже давно надоевший нам модуль вычисления площади треугольника по трем сторонам по формуле Герона, который мы тестировали в прошлых статьях. Там мы ограничились тестированием готового кода, поскольку главной целью тогда было изучение основ модульного тестирования в общем и инструмента под названием Unity в частности. С тех пор мы изрядно поднаторели в этом деле и наконец-то готовы перейти на новый уровень разработки — использование технологии РУТ.

Подготовительная часть

Постановка задачи

Итак, наша работа в этот раз начнется с того, с чего и должна начинаться реальная разработка, а именно — с получения спецификации. Для нашего учебного примера, конечно же, и спецификация будет под стать, игрушечная; о том, как должны выглядеть спецификации реальных продуктов, можно прочитать в статье «Рекомендации IEEE по разработке требований к программному обеспечению» (части 1, 2, 3).
Мы получаем уведомление о получении нового письма. Открываем почту и видим послание от менеджера проекта. Открываем его, читаем:

Цитата
«Необходимо реализовать модуль для вычисления площади треугольника по трем сторонам. Сигнатура функции:
double triangleArea(double a, double b, double c);
где a, b и c — длины сторон треугольника. Функция должна возвращать величину площади треугольника, если ее возможно вычислить.
В случае, если длина хотя бы одной из сторон треугольника отрицательна или равна нулю, модуль должен сгенерировать исключение с кодом INVALID_SIDE.
В случае, если длина какой-либо из сторон треугольника больше суммы длин двух других сторон, модуль должен сгенерировать исключение с кодом BAD_TRIANGLE.
Для генерации исключений использовать CException.
Пожалуйста, уделите особое внимание качеству продукта. Модуль будет использован в составе ПО микроконтроллера, встроенного в труднодоступный прибор. Смена прошивки в случае обнаружения ошибки крайне нежелательна.
P.S. Во вложении находится файл ErrorCode.h с определениями кодов исключений.»

Поскольку в задании явно сделан акцент на качество, мы принимаем решение использовать технологию РУТ, так как именно она способна обеспечить высшее качество кода.

Подготовка инфраструктуры

Подготавливаем фронт работ (см. рис. 1).
Рис. 1. Структура директорий проекта
Как и раньше, помещаем продукты сторонних поставщиков в поддиректорию vendor; в поддиректорию src переносим вложенный в письмо файл ErrorCode.h.
Затем создаем заготовки модуля и набора тестов к нему (аналогично тому, как мы это уже делали в прошлой статье). Добавим сигнатуру функции triangleArea в файл TriangleArea.h:

Код: (C) TriangleArea.h
#ifndef _TRIANGLEAREA_H
#define _TRIANGLEAREA_H

double triangleArea(double a, double b, double c);

#endif // _TRIANGLEAREA_H

Еще нам понадобится «пустышка» для того, чтобы компиляция не завершалась с ошибкой:

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

double triangleArea(double a, double b, double c)
{
  return 0.0;
}

На этом подготовительный этап закончен.

Урок РУТ

Пишем первый тест

Не забываем, что в этот раз мы работаем в полном согласии с технологией РУТ, поэтому мы не имеем права писать ни строчки кода до тех пор, пока у нас нет хотя бы одного проваленного теста. Предыдущие заголовочные файлы не в счет, поскольку они лишь содержат объявления в соответствии с полученными нами спецификациями.
Перед началом работы нужно определиться с одним чисто процедурным вопросом. Мы уже пришли к выводу, что будем писать тесты перед кодом. Но в какой именно последовательности мы будем их писать? На этот счет среди сторонников РУТ нет единой точки зрения.
Некоторые предпочитают написать все тесты сразу, а потом добавлять понемногу код, стремясь к тому, чтобы на каждом шаге количество проваленных тестов уменьшалось хотя бы на единицу. Другие пишут за раз один-два теста, потом код, который их проходит. Третьи выбирают компромиссное решение: сразу делают пустые заготовки для всех планируемых тестов, чтобы ничего не забыть, а потом реализуют их по одному.
Первый подход мне не нравится тем, что мы сразу же тонем в множестве проваленных тестов, и поначалу трудно решить, за что хвататься в первую очередь. Третий подход привлекателен для людей, не привыкших полагаться на память, поскольку тесты-пустышки образуют своеобразный план работ. Для небольших задач с простыми спецификациями, как в нашем случае, можно ограничиться вторым вариантом. Его мы и выберем.
Начнем, как и положено, с написания теста. Но перед этим выберем требование, которое мы намерены проверить. После первого просмотра спецификации появляется мысль, что хорошо бы сначала проверить входные параметры на предмет допустимости, а уж затем пытаться посчитать саму формулу.
Выбираем требование: «В случае, если длина хотя бы одной из сторон треугольника отрицательна или равна нулю, модуль должен сгенерировать исключение с кодом INVALID_SIDE». Его достаточно легко проверить; с него и начнем:

Код: (C) TestTriangleArea.c
#include "unity.h"
#include "TriangleArea.h"

void setUp(void)
{
}

void tearDown(void)
{
}

void test_Side_1_isLessThan_0_Exception(void)
{
  // фаза 1 - подготовка
  CEXCEPTION_T e;
  // фаза 2 - выполнение
  Try
  {
    double actual = triangleArea(-3.0, 4.0, 5.0);
    // фаза 3 - оценка
    // если мы попали в эту точку, то обработка ошибок не работает
    TEST_FAIL_MESSAGE("No exception was thrown");
  }
  Catch(e)
  {
    // проверяем, что исключение "правильное"
    TEST_ASSERT_EQUAL_UINT((unsigned int) INVALID _SIDE, (unsigned int)e);
  }
}

Обратите внимание, что мы не экономим на названиях функций. Название функции должно говорить о ее назначении, тогда не придется писать дополнительные комментарии для пояснения своих намерений. Комментарии сами по себе не есть зло; но при изменении кода смысл комментариев может утратиться, а то и вовсе ввести читателя в заблуждение, если комментарии не меняются синхронно с кодом. Поддержание этой синхронности требует усилий и времени; следовательно, по возможности следует воздерживаться от лишних комментариев. Если код говорит сам за себя, это как раз тот самый случай, когда комментарии излишни. В нашем случае название тестовой функции недвусмысленно намекает на то, что проверяется генерация исключения в случае, если длина первой стороны треугольника меньше 0.
Генерируем test runner, запускаем:

../../../TriangleArea/test/TestTriangleArea.c:26:test_Side_1_isLessThan_0_Exception:FAIL: No exception was thrown
-----------------------
1 Tests 1 Failures 0 Ignored
FAIL

Вот теперь у нас есть проваленный тест, и мы имеем полное право писать основной код в модуле, не прошедшем тестирование.

Модифицируем тестируемую функцию

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

Код: (C) TriangleArea.c
#include "TriangleArea.h"
#include "ErrorCode.h"

double triangleArea(double a, double b, double c)
{
  Throw(INVALID_SIDE);
}

../../../TriangleArea/test/TestTriangleArea.c:12:test_Side_1_isLessThan_0_Exception:PASS
-----------------------
1 Tests 0 Failures 0 Ignored
OK

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

Второй тест

Очевидно, что в данный момент добавление тестов для отрицательных длин второй и третьей сторон никакого конструктива не добавит, ведь эти тесты также завершатся успешно, а значит, мы ни на шаг не продвинемся к цели, так как без проваленных тестов мы не можем писать код. Вместо этого выберем другой пункт спецификации:
«В случае, если длина какой-либо из сторон треугольника больше суммы длин двух других сторон, модуль должен сгенерировать исключение с кодом BAD_TRIANGLE.»
Именно это условие будет проверять наш второй тест. Уж он-то наверняка закончится полным провалом, а значит, работа сдвинется с мертвой точки.

Код: (C) TestTriangleArea.c
void test_Side_1_isTooLong_Exception(void)
{
  // фаза 1 - подготовка
  CEXCEPTION_T e;
  // фаза 2 - выполнение
  Try
  {
    double actual = triangleArea(10.0, 1.0, 1.0);
    // фаза 3 - оценка
    // если мы попали в эту точку, то обработка ошибок не работает
    TEST_FAIL_MESSAGE("No exception was thrown");
  }
  Catch(e)
  {
    // проверяем, что исключение "правильное"
    TEST_ASSERT_EQUAL_UINT((unsigned int)BAD_TRIANGLE, (unsigned int)e);
  }
}

Вот он, долгожданный провал:

../../../TriangleArea/test/TestTriangleArea.c:16:test_Side_1_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:50:test_Side_1_isTooLong_Exception:FAIL: Expected 1 Was 0
-----------------------
2 Tests 1 Failures 0 Ignored
FAIL

Теперь у нас развязаны руки, и мы можем вновь приступать к кодированию. Но не забываем об умеренности: ничего лишнего не пишем, как бы у нас ни чесались руки.

Вторая правка

Проанализировав результаты прогона второго теста и просмотрев ошибочный код, мы приходим к мысли, что причина неудачи — безусловная генерация исключения, невзирая на исходные данные. Очевидно, что исключение следует генерировать лишь в том случае, если величина первого параметра отрицательна:

Код: (C) TriangleArea.c
double triangleArea(double a, double b, double c)
{
  if (a < 0)
    Throw(INVALID_SIDE);

  Throw(BAD_TRIANGLE);
}

Опять все по минимуму, ничего лишнего; Оккам гордился бы нами, если бы дожил. Результаты прогона набора тестов тоже радуют:

../../../TriangleArea/test/TestTriangleArea.c:16:test_Side_1_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:35:test_Side_1_isTooLong_Exception:PASS
-----------------------
2 Tests 0 Failures 0 Ignored
OK

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

Рефакторинг

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

Код: (C) TestTriangleArea.c
#include "unity.h"
#include "TriangleArea.h"
#include "CException.h"
#include "ErrorCode.h"

void setUp(void)
{
}

void tearDown(void)
{
}

void test_Side_1_isLessThan_0_Exception(void)
{
  // фаза 1 - подготовка
  CEXCEPTION_T e;
  // фаза 2 - выполнение
  Try
  {
    double actual = triangleArea(-3.0, 4.0, 5.0);
    // фаза 3 - оценка
    // если мы попали в эту точку, то обработка ошибок не работает
    TEST_FAIL_MESSAGE("No exception was thrown");
  }
  Catch(e)
  {
    // проверяем, что исключение "правильное"
    TEST_ASSERT_EQUAL_UINT((unsigned int)INVALID_SIDE, (unsigned int)e);
  }
}

void test_Side_1_isTooLong_Exception(void)
{
  // фаза 1 - подготовка
  CEXCEPTION_T e;
  // фаза 2 - выполнение
  Try
  {
    double actual = triangleArea(10.0, 1.0, 1.0);
    // фаза 3 - оценка
    // если мы попали в эту точку, то обработка ошибок не работает
    TEST_FAIL_MESSAGE("No exception was thrown");
  }
  Catch(e)
  {
    // проверяем, что исключение "правильное"
    TEST_ASSERT_EQUAL_UINT((unsigned int)BAD_TRIANGLE, (unsigned int)e);
  }
}

…и после рефакторинга:

Код: (C) TestTriangleArea.c
#include "unity.h"
#include "TriangleArea.h"
#include "CException.h"
#include "ErrorCode.h"

void setUp(void)
{
}

void tearDown(void)
{
}

static void callTriangleAreaAndCheckException(
  double a, double b, double c, CEXCEPTION_T ce)
{
  CEXCEPTION_T e;
  Try
  {
    triangleArea(a, b, c);
    TEST_FAIL_MESSAGE("No exception was thrown");
  }
  Catch(e)
  {
    TEST_ASSERT_EQUAL_UINT((unsigned int)ce, (unsigned int)e);
  }
}

void test_Side_1_isLessThan_0_Exception(void)
{
  callTriangleAreaAndCheckException(-3.0, 4.0, 5.0, INVALID_SIDE);
}

void test_Side_1_isTooLong_Exception(void)
{
  callTriangleAreaAndCheckException(10.0, 1.0, 1.0, BAD_TRIANGLE);
}

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

../../../TriangleArea/test/TestTriangleArea.c:16:test_Side_1_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:35:test_Side_1_isTooLong_Exception:PASS
-----------------------
2 Tests 0 Failures 0 Ignored
OK

Полный тест отрицательных длин

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

Код: (C) TestTriangleArea.c
void test_Side_2_isLessThan_0_Exception(void)
{
  callTriangleAreaAndCheckException(3.0, -4.0, 5.0, INVALID_SIDE);
}

../../../TriangleArea/test/TestTriangleArea.c:31:test_Side_1_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:36:test_Side_1_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:27:test_Side_2_isLessThan_0_Exception:FAIL: Expected 0 Was 1
-----------------------
3 Tests 1 Failures 0 Ignored
FAIL

Наличие сбойного теста дает нам право вернуться к целевому коду:

Код: (C) TriangleArea.c
double triangleArea(double a, double b, double c)
{
  if (
        (a < 0)
     || (b < 0)
     )
    Throw(INVALID_SIDE);

  Throw(BAD_TRIANGLE);
}

../../../TriangleArea/test/TestTriangleArea.c:31:test_Side_1_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:36:test_Side_1_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:41:test_Side_2_isLessThan_0_Exception:PASS
-----------------------
3 Tests 0 Failures 0 Ignored
OK

Повторяем:

Код: (C) TestTriangleArea.c
void test_Side_3_isLessThan_0_Exception(void)
{
  callTriangleAreaAndCheckException(3.0, 4.0, -5.0, INVALID_SIDE);
}

../../../TriangleArea/test/TestTriangleArea.c:31:test_Side_1_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:36:test_Side_1_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:41:test_Side_2_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:27:test_Side_3_isLessThan_0_Exception:FAIL: Expected 0 Was 1
-----------------------
4 Tests 1 Failures 0 Ignored
FAIL

Код: (C) TriangleArea.c
double triangleArea(double a, double b, double c)
{
  if (
         (a < 0)
      || (b < 0)
      || (c < 0)
      )
    Throw(INVALID_SIDE);

  Throw(BAD_TRIANGLE);
}

../../../TriangleArea/test/TestTriangleArea.c:31:test_Side_1_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:36:test_Side_1_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:41:test_Side_2_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:46:test_Side_3_isLessThan_0_Exception:PASS
-----------------------
4 Tests 0 Failures 0 Ignored
OK

Тесты нулевых длин

Без лишних комментариев продолжаем в том же духе:
Тест:

Код: (C) TestTriangleArea.c
void test_Side_1_isEqual_0_Exception(void)
{
  callTriangleAreaAndCheckException(0.0, 4.0, 5.0, INVALID_SIDE);
}

Провал:

../../../TriangleArea/test/TestTriangleArea.c:31:test_Side_1_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:36:test_Side_1_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:41:test_Side_2_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:46:test_Side_3_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:27:test_Side_1_isEqual_0_Exception:FAIL: Expected 0 Was 1
-----------------------
5 Tests 1 Failures 0 Ignored
FAIL

Правка:

Код: (C) TriangleArea.c
double triangleArea(double a, double b, double c)
{
  if (
         (a <= 0)
      || (b < 0)
      || (c < 0)
      )
    Throw(INVALID_SIDE);

  Throw(BAD_TRIANGLE);
}

Успех:

../../../TriangleArea/test/TestTriangleArea.c:31:test_Side_1_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:36:test_Side_1_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:41:test_Side_2_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:46:test_Side_3_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:51:test_Side_1_isEqual_0_Exception:PASS
-----------------------
5 Tests 0 Failures 0 Ignored
OK

Остальные стороны

Дадим себе в этот раз маленькое послабление в виде двух однотипных тестов сразу:

Код: (C) TestTriangleArea.c
void test_Side_2_isEqual_0_Exception(void)
{
  callTriangleAreaAndCheckException(3.0, 0.0, 5.0, INVALID_SIDE);
}

void test_Side_3_isEqual_0_Exception(void)
{
  callTriangleAreaAndCheckException(3.0, 4.0, 0.0, INVALID_SIDE);
}

../../../TriangleArea/test/TestTriangleArea.c:31:test_Side_1_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:36:test_Side_1_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:41:test_Side_2_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:46:test_Side_3_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:51:test_Side_1_isEqual_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:27:test_Side_2_isEqual_0_Exception:FAIL: Expected 0 Was 1
../../../TriangleArea/test/TestTriangleArea.c:27:test_Side_3_isEqual_0_Exception:FAIL: Expected 0 Was 1
-----------------------
7 Tests 2 Failures 0 Ignored
FAIL

Код: (C) TriangleArea.c
double triangleArea(double a, double b, double c)
{
  if (
         (a <= 0)
      || (b <= 0)
      || (c <= 0)
      )
    Throw(INVALID_SIDE);

  Throw(BAD_TRIANGLE);
}

../../../TriangleArea/test/TestTriangleArea.c:31:test_Side_1_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:36:test_Side_1_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:41:test_Side_2_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:46:test_Side_3_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:51:test_Side_1_isEqual_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:56:test_Side_2_isEqual_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:61:test_Side_3_isEqual_0_Exception:PASS
-----------------------
7 Tests 0 Failures 0 Ignored
OK

Тесты оставшихся некорректных сочетаний длин сторон


Код: (C) TestTriangleArea.c
void test_Side_2_isTooLong_Exception(void)
{
  callTriangleAreaAndCheckException(1.0, 10.0, 1.0, BAD_TRIANGLE);
}

void test_Side_3_isTooLong_Exception(void)
{
  callTriangleAreaAndCheckException(1.0, 1.0, 10.0, BAD_TRIANGLE);
}

../../../TriangleArea/test/TestTriangleArea.c:31:test_Side_1_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:36:test_Side_1_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:41:test_Side_2_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:46:test_Side_3_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:51:test_Side_1_isEqual_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:56:test_Side_2_isEqual_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:61:test_Side_3_isEqual_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:66:test_Side_2_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:71:test_Side_3_isTooLong_Exception:PASS
-----------------------
9 Tests 0 Failures 0 Ignored
OK

Неожиданный результат! Мы добавили в набор два очередных теста в соответствии со спецификацией, а они завершились успешно, хотя мы не написали ни строчки кода для этого.
Что ж, бывает порой и так. Иногда причина этого — избыточность тестового набора, когда многократно проверяется одна и та же ветка кода с разными исходными значениями. Естественно, что после первого прошедшего теста остальные также пройдут, поскольку необходимый для этого код уже добавлен к программе.
Следует ли делать тестовый набор избыточным? Если вы сомневаетесь в правильности поведения программы при каком-то наборе параметров, то вполне имеет смысл развеять эти сомнения при помощи дополнительного теста. Но злоупотреблять избыточностью не следует, ведь сходные тесты и отказывать будут все сразу, не давая разработчику никакой существенно новой информации о природе дефекта, а лишь загромождая логи тестирования. Старайтесь поддерживать набор тестов полным, но при этом минимальным.
Иногда для этого придется применить некоторую изобретательность. Допустим, мы тестируем функцию возведения величины в куб. Следовало бы убедиться, что функция корректно работает при нулевых, положительных и отрицательных значениях. Можно написать три теста для значений -1, 0, 1. Но эти значения не очень хороши, поскольку 13=1, то есть невозможно определить, реализована ли функция корректно или просто возвращает свой аргумент неизменным (кстати, именно этого и требует правило написания минимально необходимого кода для прохождения набора тестов). То же относится и к случаю -1. Достаточно выбрать, скажем, набор значений -2, 0, 2, и тот же набор тестов станет информативнее.
Но у нас сейчас явно не тот случай. Мы не допустили никакой избыточности, реализовав новые тесты согласно не охваченным ранее пунктам спецификации. Тем не менее они прошли.
Придется нам последовать непреложному правилу — не писать никакой код, пока нет проваленных тестов. Добавим еще один тест, а там посмотрим.

Тест вырожденного треугольника

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

Код: (C) TestTriangleArea.c
void test_TriangleArea_Height_is_0(void)
{
  CEXCEPTION_T e;
  char buffer[80];
  double const expected = 0.0;

  Try
  {
    double actual = triangleArea(1.0, 1.0, 2.0);
    TEST_ASSERT_EQUAL_FLOAT(expected, actual);
  }
  Catch(e)
  {
    sprintf(buffer, "Unexpected exception: %u", e);
    TEST_FAIL_MESSAGE(buffer);
  }
}

../../../TriangleArea/test/TestTriangleArea.c:31:test_Side_1_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:36:test_Side_1_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:41:test_Side_2_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:46:test_Side_3_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:51:test_Side_1_isEqual_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:56:test_Side_2_isEqual_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:61:test_Side_3_isEqual_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:66:test_Side_2_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:71:test_Side_3_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:91:test_TriangleArea_Height_is_0:FAIL: Unexpected exception: 1
-----------------------
10 Tests 1 Failures 0 Ignored
FAIL

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

Код: (C) TriangleArea.c
double triangleArea(double a, double b, double c)
{
  if (
         (a <= 0)
      || (b <= 0)
      || (c <= 0)
      )
    Throw(INVALID_SIDE);

  if (a > b + c)
    Throw(BAD_TRIANGLE);

  return 0.0;
}

Результат несколько озадачивает:

../../../TriangleArea/test/TestTriangleArea.c:31:test_Side_1_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:36:test_Side_1_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:41:test_Side_2_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:46:test_Side_3_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:51:test_Side_1_isEqual_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:56:test_Side_2_isEqual_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:61:test_Side_3_isEqual_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:24:test_Side_2_isTooLong_Exception:FAIL: No exception was thrown
../../../TriangleArea/test/TestTriangleArea.c:24:test_Side_3_isTooLong_Exception:FAIL: No exception was thrown
../../../TriangleArea/test/TestTriangleArea.c:76:test_TriangleArea_Height_is_0:PASS
-----------------------
10 Tests 2 Failures 0 Ignored
FAIL

Мы добились того, что последний тест сработал, но зато провалились два другие, которые до этого работали. Что ж, это нормальная ситуация. В процессе модификации кода изменения порой нарушают функциональность ранее успешно работавших фрагментов. Именно поэтому мы каждый раз добавляем тест к набору и выполняем весь набор целиком, а не только один новый тест.
Процесс, когда ранее написанный код постоянно тестируется снова и снова, называется регрессионным тестированием. Цель регрессионного тестирования — убедиться, что в процессе развития продукта никакая ранее реализованная функциональность не утрачена, или, другими словами, код не подвергся регрессии. Это самым благоприятным образом сказывается на качестве результирующего продукта.
Понятно, что производить полноценное регрессионное тестирование реально возможно лишь при двух условиях: 1) процесс тестирования должен быть полностью автоматизирован; 2) он должен происходить достаточно быстро, отнимая у разработчика незначительную часть времени. Природа этих условий очевидна. Ручное тестирование трудоемко, рутинно, требует постоянной высокой концентрации внимания и длится долго. Вполне естественно, что разработчики будут избегать такой работы всеми мыслимыми способами. Кроме того, если регрессионное тестирование будет занимать существенную часть рабочего времени, регресса, конечно, мы сумеем избежать, но и особого прогресса тоже не достигнем. К счастью, оба эти условия у нас соблюдаются.
Вернемся к нашим регрессировавшим тестам. Очевидно, что причиной «порчи» кода стало неполнота условной логики выброса исключения:

Код: (C) TriangleArea.c
double triangleArea(double a, double b, double c)
{
  if (
         (a <= 0)
      || (b <= 0)
      || (c <= 0)
      )
    Throw(INVALID_SIDE);

  if (
         (a > b + c)
      || (b > a + c)
      || (c > a + b)
      )
    Throw(BAD_TRIANGLE);

  return 0.0;
}

../../../TriangleArea/test/TestTriangleArea.c:31:test_Side_1_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:36:test_Side_1_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:41:test_Side_2_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:46:test_Side_3_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:51:test_Side_1_isEqual_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:56:test_Side_2_isEqual_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:61:test_Side_3_isEqual_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:66:test_Side_2_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:71:test_Side_3_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:76:test_TriangleArea_Height_is_0:PASS
-----------------------
10 Tests 0 Failures 0 Ignored
OK

Статус кво восстановлен.

Последний тест

До сих пор мы тестировали что угодно, кроме основной функции, ради которой модуль и создается. Впрочем, у нас есть оправдание в виде повышенных требований к надежности модуля, поэтому мелочей здесь нет, ничем пренебрегать нельзя. Но пришло время и для формулы Герона.

Код: (C) TestTriangleArea.c
void test_TriangleArea_3_4_5_Equals_6(void)
{
  CEXCEPTION_T e;
  char buffer[80];
  double const expected = 6.0;

  Try
  {
    double actual = triangleArea(3.0, 4.0, 5.0);
    TEST_ASSERT_EQUAL_FLOAT(expected, actual);
  }
  Catch(e)
  {
    sprintf(buffer, "Unexpected exception: %u", e);
    TEST_FAIL_MESSAGE(buffer);
  }
}

../../../TriangleArea/test/TestTriangleArea.c:32:test_Side_1_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:37:test_Side_1_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:42:test_Side_2_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:47:test_Side_3_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:52:test_Side_1_isEqual_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:57:test_Side_2_isEqual_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:62:test_Side_3_isEqual_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:67:test_Side_2_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:72:test_Side_3_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:77:test_TriangleArea_Height_is_0:PASS
../../../TriangleArea/test/TestTriangleArea.c:104:test_TriangleArea_3_4_5_Equals_6:FAIL: Values Not Within Delta
-----------------------
11 Tests 1 Failures 0 Ignored
FAIL

Код: (C) TriangleArea.c
double triangleArea(double a, double b, double c)
{
  if (
         (a <= 0)
      || (b <= 0)
      || (c <= 0)
      )
    Throw(INVALID_SIDE);

  if (
         (a > b + c)
      || (b > a + c)
      || (c > a + b)
      )
    Throw(BAD_TRIANGLE);

  double p = (a + b + c) / 2.0;
  double result = sqrt(p * (p - a) * (p - b) * (p - c));
  return result;
}

../../../TriangleArea/test/TestTriangleArea.c:32:test_Side_1_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:37:test_Side_1_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:42:test_Side_2_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:47:test_Side_3_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:52:test_Side_1_isEqual_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:57:test_Side_2_isEqual_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:62:test_Side_3_isEqual_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:67:test_Side_2_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:72:test_Side_3_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:77:test_TriangleArea_Height_is_0:PASS
../../../TriangleArea/test/TestTriangleArea.c:95:test_TriangleArea_3_4_5_Equals_6:PASS
-----------------------
11 Tests 0 Failures 0 Ignored
OK

Наводим последний марафет

Все тесты из нашей спецификации успешно пройдены. В принципе можно почивать на лаврах. Вот только опять потянуло знакомым запахом… Конечно, можно было бы его игнорировать, но мы не поддадимся этому соблазну. Стремиться к совершенству следует не только в целевом коде, но и в тестах. В конце концов, неизвестно, какое будущее уготовано нашему проекту, и ему может потребоваться длительное сопровождение. Поэтому вернемся к рефакторингу.
Это дело нам уже хорошо знакомо:

Код: (C) TestTriangleArea.c
static void checkTriangleAreaWithoutException(
  double a, double b, double c, double expectedResult)
{
  CEXCEPTION_T e;
  char buffer[80];

  Try
  {
    double actual = triangleArea(a, b, c);
    TEST_ASSERT_EQUAL_FLOAT(expectedResult, actual);
  }
  Catch(e)
  {
    sprintf(buffer, "Unexpected exception: %u", e);
    TEST_FAIL_MESSAGE(buffer);
  }
}

void test_TriangleArea_Height_is_0(void)
{
  checkTriangleAreaWithoutException(1.0, 1.0, 2.0, 0.0);
}

void test_TriangleArea_3_4_5_Equals_6(void)
{
  checkTriangleAreaWithoutException(3.0, 4.0, 5.0, 6.0);
}

../../../TriangleArea/test/TestTriangleArea.c:32:test_Side_1_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:37:test_Side_1_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:42:test_Side_2_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:47:test_Side_3_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:52:test_Side_1_isEqual_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:57:test_Side_2_isEqual_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:62:test_Side_3_isEqual_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:67:test_Side_2_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:72:test_Side_3_isTooLong_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:77:test_TriangleArea_Height_is_0:PASS
../../../TriangleArea/test/TestTriangleArea.c:95:test_TriangleArea_3_4_5_Equals_6:PASS
-----------------------
11 Tests 0 Failures 0 Ignored
OK

Вот теперь можно ставить точку.

Итоговый код функции:

Код: (C) TriangleArea.c
#include "TriangleArea.h"
#include "ErrorCode.h"

double triangleArea(double a, double b, double c)
{
  if (
         (a <= 0)
      || (b <= 0)
      || (c <= 0)
      )
    Throw(INVALID_SIDE);

  if (
         (a > b + c)
      || (b > a + c)
      || (c > a + b)
      )
    Throw(BAD_TRIANGLE);

  double p = (a + b + c) / 2.0;
  double result = sqrt(p * (p - a) * (p - b) * (p - c));
  return result;
}

Тестовый набор:

Код: (C) TestTriangleArea.c
#include "unity.h"
#include "TriangleArea.h"
#include "CException.h"
#include "ErrorCode.h"
#include <stdio.h>

void setUp(void)
{
}

void tearDown(void)
{
}

static void callTriangleAreaAndCheckException(
  double a, double b, double c, CEXCEPTION_T ce)
{
  CEXCEPTION_T e;
  Try
  {
    triangleArea(a, b, c);
    TEST_FAIL_MESSAGE("No exception was thrown");
  }
  Catch(e)
  {
    TEST_ASSERT_EQUAL_UINT((unsigned int)ce, (unsigned int)e);
  }
}

void test_Side_1_isLessThan_0_Exception(void)
{
  callTriangleAreaAndCheckException(-3.0, 4.0, 5.0, INVALID_SIDE);
}

void test_Side_1_isTooLong_Exception(void)
{
  callTriangleAreaAndCheckException(10.0, 1.0, 1.0, BAD_TRIANGLE);
}

void test_Side_2_isLessThan_0_Exception(void)
{
  callTriangleAreaAndCheckException(3.0, -4.0, 5.0, INVALID_SIDE);
}

void test_Side_3_isLessThan_0_Exception(void)
{
  callTriangleAreaAndCheckException(3.0, 4.0, -5.0, INVALID_SIDE);
}

void test_Side_1_isEqual_0_Exception(void)
{
  callTriangleAreaAndCheckException(0.0, 4.0, 5.0, INVALID_SIDE);
}

void test_Side_2_isEqual_0_Exception(void)
{
  callTriangleAreaAndCheckException(3.0, 0.0, 5.0, INVALID_SIDE);
}

void test_Side_3_isEqual_0_Exception(void)
{
  callTriangleAreaAndCheckException(3.0, 4.0, 0.0, INVALID_SIDE);
}

void test_Side_2_isTooLong_Exception(void)
{
  callTriangleAreaAndCheckException(1.0, 10.0, 1.0, BAD_TRIANGLE);
}

void test_Side_3_isTooLong_Exception(void)
{
  callTriangleAreaAndCheckException(1.0, 1.0, 10.0, BAD_TRIANGLE);
}

static void checkTriangleAreaWithoutException(
  double a, double b, double c, double expectedResult)
{
  CEXCEPTION_T e;
  char buffer[80];

  Try
  {
    double actual = triangleArea(a, b, c);
    TEST_ASSERT_EQUAL_FLOAT(expectedResult, actual);
  }
  Catch(e)
  {
    sprintf(buffer, "Unexpected exception: %u", e);
    TEST_FAIL_MESSAGE(buffer);
  }
}

void test_TriangleArea_Height_is_0(void)
{
  checkTriangleAreaWithoutException(1.0, 1.0, 2.0, 0.0);
}

void test_TriangleArea_3_4_5_Equals_6(void)
{
  checkTriangleAreaWithoutException(3.0, 4.0, 5.0, 6.0);
}

Выводы

Итак, перечислю вкратце, чему мы научились в ходе проделанной работы.
Наш процесс разработки базировался на тестах. Но и сами тесты возникли не на пустом месте; мы составляли их на основе спецификации требований к модулю. Прежде, чем приступать к работе, нужно составить четкое представление о том, что мы хотим получить в итоге. Именно на этот вопрос и отвечают спецификации.
Мы очень жестко придерживались правила: не писать код, пока нет ни одного проваленного теста. Отсутствие проваленных тестов автоматически означает, что код выполняет все свои функции, а раз так, то и нечего его трогать.
Такой подход вырабатывает весьма ценную привычку: прежде чем начать что-то делать, подумайте, как будете проверять результат своей работы. Если вы затрудняетесь с проверкой, значит, вы еще не созрели и до самой работы.
Остановитесь и подумайте, пока задача не прояснится. Тактика «начну писать, а там и мысли придут» приводит, как правило, к отвратительному коду, хоть в этом и не принято себе признаваться. Пока мысли наконец-то придут, у нас уже будет ворох никчемных исходников.
Каждый тест должен проверять какой-то один небольшой аспект поведения. Если написать один «жадный» тест, который стремится проверить все в одиночку, его информативность будет минимальной: мы знаем, что модуль в целом не работает, но не имеем понятия, что именно отказало. Придется пускать в ход отладчик. Грамотное использование тестов позволяет обходиться вовсе без отладки.
Разрабатывать программу следует мелкими шагами. За один раз мы добавляем лишь один-два теста, затем заставляем их работать. Как правило, для этого достаточно добавить либо изменить лишь несколько строк кода, основная часть остается неизменной.
Каждый раз мы писали лишь необходимый для прохождения тестового набора минимум кода, даже если очевидная логика подталкивала дописать что-то еще. Нужно привыкать не поддаваться такому искушению. В результате мы получили очень простой код, в котором нет ничего лишнего. В то же время он реально работает, о чем говорят нам успешно завершившиеся тесты. Никогда не пишите код впрок в надежде, что он пригодится в будущем. Даже если и пригодится, вы вряд ли его найдете в нужный момент.
После каждого вмешательства в код обязательно производите регрессионное тестирование. Проверяйте не только добавленный/измененный фрагмент, но и весь модуль в целом. Взаимосвязи между разными частями кода порой весьма тонки и неочевидны. Регрессионное тестирование подстраховывает нас от неприятных сюрпризов.

Процедурные замечания

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

Работа с требованиями

В нашем примере требований было немного, и получили мы их якобы простым списком в обычном письме. В реальных проектах требований бывает гораздо больше, и учесть их все становится проблематично без организованной работы с ними. Получить общее представление о том, как должны выглядеть спецификации требований, можно в стандарте!!!
Одно из важнейших «требований к требованиям» — их трассируемость. Это означает, что мы без особых усилий должны иметь возможность, во-первых, определить источник данного требования, во-вторых, определить способ его реализации. Для этого каждое требование должно иметь уникальный идентификатор. Мы обходились без них, но даже в таком небольшом объеме было довольно проблематично ссылаться на конкретное требование.
Хорошая практика — указывать для каждого теста, какое (какие) требование он предназначен проверять. Обычно это несложно сделать, если тесты просты и делают мало проверок.
Работать с требованиями в виде простого текста не слишком удобно. Лучше использовать для этого вспомогательные инструменты. Например, требования можно оформить в виде заявок на изменение функциональности в сервере управления проектами Redmine.

Управление версиями

Общеизвестно, что использование системы управления версиями (СУВ) для хранения файлов проекта избавляет от множества проблем. К сожалению, сами по себе СУВ — это довольно сложные инструменты, которые требуют определенных навыков для работы. Отсутствие этих навыков приводит к тому, что управление версиями используется как замена обычному резервному копированию проекта. Тоже необходимая вещь, конечно, но полностью потенциал СУВ не раскрывает.
Применение РУТ оказывает влияние на стратегии работы с СУВ. Самое важное правило: код не должен помещаться в репозиторий до тех пор, пока не пройдет успешно все тесты. Это дополнительно стимулирует написание коротких тестов и развитие проекта частыми мелкими итерациями по схеме: написал тест — провал — написал код — успех — рефакторинг — успех — commit.

Покрытие кода тестами

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



Литература

Версия для печати
Обсудить на форуме (6)