Предисловие

Причиной этому посту послужило моё всё более и более религиозное отношение к TDD.

Недавно состоялся диалог меня (ivalentinee) и одного моего друга (SomeoneFromTheInternet):

ivalentinee: Рекомендую проникнуться духом TDD.

SomeoneFromTheInternet: TDD всё-таки требует дисциплины.

ivalentinee: Ни в коем случае не пропагандирую абсолютный TDD.

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

SomeoneFromTheInternet: Думаю, что надо просто при описании логики писать тесты.

SomeoneFromTheInternet: Типа думать, где наебнётся и что лучше бы это проверить.

Это только один пример из многих. Я уже много раз сталкивался с тем, что люди понимают TDD неправильно не так, как я.

Заранее надо предупредить о трёх вещах:

  1. Это моё понимание TDD. Не претендую на истину. Я же не какой-нибудь там Боб Мартин.
  2. Описываемое мной понимание TDD является утрированным и извращённым. В реальной жизни грань между “правильным” и “неправильным” TDD размывается до неразличимости.
  3. Писать примеры буду на псевдокоде русскими словами. И буду склонять названия. Потому что могу, вот почему.

Что ж, поехали.

Что такое TDD

Как следует из расшифровки аббревиатуры TDD — Test Driven Development. По-русски это будет примерно «Разработка через тестирование». Так вот, самое важное слово в данном случае не “тестирование” (которое стоит на последнем месте), а “разработка” (которое стоит на первом месте).

Из вышесказанного вытекает следующий неочевидный вывод: TDD это не про тестирование, а про разработку, про написание кода.

Приведу для красного словца известное описание TDD:

  1. Красный
  2. Зелёный
  3. Рефакторинг

Отмечу, что данный сценарий является циклическим.

Какие проблемы не решает TDD

  1. Гарантированная безошибочность приложения.
  2. Гарантированное покрытие тестами > n%.

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

Какие проблемы решает TDD

  1. Изолированность написания модулей приложения.
  2. Скорость написания кода.

Чуть ниже все четыре пункта рассмотрю подробнее, а пока опишу модель TDD.

Модель TDD

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

Итак, предположим, что у нас есть некоторое готовое приложение. И тут к нам приходит заказчик/начальник/уборщица/кто-угодно и говорит: “нам нужно, чтобы приложение делало ещё вот_это_вот”.

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

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

Разберём на примере выбора самого высокого человека из предоставленного ряда. Человек будет объектом со свойством рост. Сильно утрированный сценарий, но так будет проще.

Красная фаза

Тут надо крепко задуматься. Ибо предстоит решить “а что мы всё-таки в результате-то хотим”? Ответ на этот вопрос и является решением красной фазы.

На примере человека становится ясно, что мы пишем такое требование:

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

Справедливое требование, не правда ли?

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

РЯД = РядСлучайныхЛюдей;

САМЫЙ_ВЫСОКИЙ_ЧЕЛОВЕК = выбратьСамогоВысокогоЧеловека(РЯД)

для каждого ВЫБРАННОГО_ЧЕЛОВЕКА из РЯДА:
  если рост(ВЫБРАННОГО_ЧЕЛОВЕКА) > рост(САМОГО_ВЫСОКОГО_ЧЕЛОВЕКА) то провалитьТест

Очевидное преобразование, не так ли?

С этого момента у программиста есть формализованный контракт к самому себе на написание кода, который в отвечает на вопрос: “Мы написали то что хотели, или нет?”

До сих пор мы не написали ни строки кода реализации.

Красная фаза пройдена.

Почему красная? Потому что проверка нашего кода по контракту выдаёт отрицательный — “красный” — результат.

Зелёная фаза

И в этой фазе нужно выполнить написанный контракт. Нужно написать решение. Какое это будет решение — не очень-то и важно, на самом деле.

Но приведу примерный псевдокод:

определение: выбратьСамогоВысокогоЧеловека(РЯД)
  выбратьПервыйЭлемент(отсортироватьПузырьком(РЯД))

Контракт программиста самому себе выполнен. Зелёная фаза пройдена.

Почему зелёная? Потому что проверка нашего кода по контракту выдаёт положительный — “зелёный” — результат.

Рефакторинг

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

Важно то, что на данном этапе не изменяется ни контракт, ни сам факт его выполнения. Изменяется только способ его выполнения.

Например, изменяем нашу реализацию таким образом:

определение: выбратьСамогоВысокогоЧеловека(РЯД)
  отсортированыйРяд = отсортироватьДвоичнойСортировкой(РЯД)
  выбратьПервыйЭлемент(отсортированыйРяд)

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

Эта фаза может совмещаться с code review, например, если это необходимо.

Фаза рефакторинга пройдена.

Что это нам даёт и чего не даёт (вышеупомянутые четыре пункта)

Изолированность написания модулей приложения

Такой способ написания кода позволяет проверять и реализовывать необходимый функционал. Контракт проверяет выполнение только выбраной нами функции и никакой другой. И не зависит от работы других функций.

Скорость написания кода

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

Гарантированная безошибочность приложения

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

Мы живём в реальном мире. А это значит, что есть две большие проблемы:

  1. Невозможно составить полный контракт на всю область определения.
  2. Внешние зависимости не обязаны отвечать требованиям контрактов приложения.

Про это я ещё чуть подробнее напишу в “советах”.

Гарантированное покрытие тестами > n%.

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

Небольшой список советов (в том числе основанных на личном опыте)

  1. Не надо пытаться писать контракты на любой сценарий. Опишите только на минимально-необходимый. И уточняйте контракт только в том случае, если есть зафиксированный баг.
  2. Не надо определять контракты для внешних зависимостей. Контракт, который проверяет dnslookup(googleDNS) не только бессмысленнен, но и вреден. Ибо будет ломаться тогда, когда приложение будет работать. Отсюда следует следующий совет:
  3. Изолируйте свои контракты. Если внутри себя функция А вызывает достаточно сложную (и ненадёжную) функцию Б из какого-то другого модуля — применяйте stub. Если нужен какой-то сложный (и ненадёжный) объект для проверки — применяйте mock-объект. Не ленитесь описывать подготовку данных для каждой проверки заново. Один из примеров библиотек для изоляции тестов — webmock — мой спаситель в очень многих ситуациях.
  4. Не пишите большие и сложные контракты. Лучше их декомпозировать. Если есть, например, две проверки на разное поведение одной функции, то это два теста, а не один с двумя проверками.
  5. Когда наберётесь опыта, можно будет в фазу рефакторинга включить не только рефакторинг кода, но и рефакторинг контракта. Правда, делать это надо осторожно и последовательно: сначала полностью заканчиваете рефакторинг кода, а потом уже рефакторите контракт.
  6. Оставьте регрессионные и интеграционные тесты команде тестирования. Потому что писать код с помощью интеграционных тестов (в том числе всякие зашкварные капибары и phantomjs’ы) — это адЪ (есть личный опыт). К тому же это будет лишней тратой времени, никак не связанной с написанием кода приложения.

Набросы (будет со временем обновляться, я надеюсь)

О чрезмерном тестировании

BDD: RSpec, Cucumber

Тестирование серого ящика

Stub и Mock

Великий webmock-спаситель — позволяет сделать некрасивые stub’ы для сторонних API.