TDD: что это и как его использовать
Метод разработки, который убирает до 80% ошибок. Но только в умелых руках.
Разработчики пишут тесты, чтобы находить ошибки до того, как с ними столкнутся пользователи IT-сервисов. Однако сами по себе тесты не гарантируют качества кода: результат зависит от того, когда их создают и как в команде выстроен процесс разработки.
TDD, или разработка через тестирование, предполагает, что тест пишут до рабочего кода. Сначала разработчик описывает ожидаемое поведение программы, а затем создаёт реализацию, которая проходит проверку. Такой порядок помогает разбивать задачу на небольшие шаги, заранее продумывать требования и постепенно улучшать код.
В этой статье мы разберём, как устроен TDD, из каких этапов он состоит, чем он отличается от традиционного тестирования и как начать применять этот подход на практике.
Содержание
- Что такое TDD
- Из каких этапов состоит разработка через тестирование
- Пишем модуль программы по принципам TDD
- Преимущества подхода
- Недостатки TDD
- Частые ошибки при использовании TDD
- Что дальше
Что такое TDD
TDD, или Test-Driven Development, — это подход к разработке, при котором сначала пишут тест, а затем — код, необходимый для его прохождения.
При традиционном подходе разработчик сначала создаёт функцию, а затем проверяет, правильно ли она работает. В этом случае тесты могут подстраиваться под уже готовую реализацию: они охватывают предусмотренные сценарии, но иногда упускают пограничные случаи — например, некорректные данные, которые может ввести пользователь.
В TDD разработчик заранее определяет, как должна работать программа, и описывает это поведение в тесте. Например, перед созданием функции сложения он задаёт условие: если передать числа 2 и 3, результат должен быть равен 5. Сначала тест завершается с ошибкой, потому что функции ещё не существует. Затем разработчик пишет минимальный код, необходимый для прохождения проверки.
После этого он добавляет новые сценарии: сложение с нулём, отрицательными числами и другими значениями. Для каждого случая цикл повторяется: сначала тест, затем код. Так требования превращаются в конкретные проверки, а разработчик реализует только ту функциональность, которая нужна на текущем этапе.
TDD активно используют крупные IT-проекты. В исследовании команд Microsoft и IBM использование этого подхода связывали со снижением количества дефектов на 40–90%. При этом первоначальная разработка занимала на 15–35% больше времени, однако дополнительные затраты частично компенсировались более простой поддержкой продукта.
Из каких этапов состоит TDD
Разработка через тестирование состоит из трёх этапов: красного, зелёного и синего. Их также называют Red, Green и Refactor.
Красный этап начинается с написания теста для функции или отдельного сценария. Этот тест должен завершиться ошибкой, потому что требуемая логика ещё не реализована на уровне кода. Так разработчик проверяет, что тест действительно работает и не даёт ложноположительного результата. Переходить к следующему этапу можно только после того, как тест ожидаемо не пройден.
Зелёный этап — написание минимального объёма кода, необходимого для прохождения теста. На этом этапе разработчик не должен продумывать идеальное техническое решение или оптимизировать архитектуру. Главная задача — добиться того, чтобы новый тест и все предыдущие завершились успешно.
Синий этап, или рефакторинг. На нём разработчик улучшает код, не меняя его поведения: устраняет дублирование, упрощает структуру, уточняет названия и повышает читаемость. После изменений тесты должны завершаться без ошибок.
Каждая новая функция или каждый новый модуль программы должны проходить этот цикл отдельно.

Цикл TDD дополняют три правила, которые сформулировал родоначальник этого подхода — Кент Бек:
- Если модульный тест завершился неудачно, следует написать код только для того, чтобы пройти этот тест.
- Не усложняйте тест больше, чем нужно для того, чтобы он провалился.
- Не пишите больше кода, чем необходимо для прохождения текущего теста.
Эти правила помогают сократить объём работы разработчиков и инженеров по тестированию, уменьшая количество кода.
Пишем модуль программы по принципам TDD
Создадим модуль, который будет проверять надёжность пароля. Сначала определим требования: без них невозможно понять, какие тесты нужно написать и какого результата ожидать от программы.
В нашем примере пароль считается надёжным, если он одновременно:
- содержит не менее восьми символов;
- включает хотя бы одну цифру;
- включает хотя бы одну заглавную и одну строчную букву.
Если хотя бы одно из условий не выполнено, пароль считается ненадёжным. Эти требования станут основой для тестов. Позже функцию можно будет расширить — например, добавить проверку на наличие специального символа. Каждое новое правило также будет реализовано по циклу TDD: сначала тест, затем код.
Пример напишем на Python с помощью библиотеки Pytest. Сначала создадим тесты для разных сценариев, а затем реализуем функцию, которая должна успешно их выполнить. Перед началом работы убедитесь, что Pytest установлена.

Читайте также:
Шаг 1: пишем красные тесты
Напишем три теста:
- на общую длину пароля;
- на наличие цифр;
- на наличие как заглавных, так и строчных букв.
Создайте папку проекта и файл test_password.py. Скопируйте в него код:
import pytest # Импортируем фреймворк для тестирования
def test_password_length():
# Тест проверяет, что пароль короче восьми символов считается слабым
# Ожидаем False, но функция ещё не реализована, так что тест провалится
assert is_strong_password("Abc1") == False
def test_has_digit():
# Тест на наличие хотя бы одной цифры
# Без цифры должен вернуть False
assert is_strong_password("Abcdefgh") == False
def test_has_upper_and_lower():
# Тест на наличие букв верхнего и нижнего регистра
# Только нижний регистр — False
assert is_strong_password("abcdefg1") == FalseЕсли сейчас запустить тесты, то они ожидаемо провалятся, так как самих функций проверки ещё нет.
Шаг 2: пишем зелёные тесты
Теперь реализуем минимальную логику, необходимую для выполнения проверок. Сначала добавим в is_strong_password условие для длины пароля. После этого первая проверка завершится успешно.
Затем расширим функцию и добавим поиск цифр с помощью выражения any (char.isdigit() for char in password). Этого достаточно, чтобы выполнить второе требование без лишнего кода.
На последнем шаге добавим проверки isupper() и islower(), чтобы убедиться, что пароль содержит заглавные и строчные буквы. После этого функция будет соответствовать всем заданным требованиям.
def is_strong_password(password):
# Минимальная проверка длины для первого теста
if len(password) < 8:
return False
# Проверка на цифру для второго теста
if not any(char.isdigit() for char in password):
return False
# Проверка на верхний и нижний регистр для третьего теста
if not (any(char.isupper() for char in password) and any(char.islower() for char in password)):
return False
return TrueШаг 3: пишем код основной программы
Теперь необходимо написать полный код модуля проверки. Создайте файл password_checker.py и скопируйте в него код:
def is_strong_password(password):
"""
Функция проверяет силу пароля по критериям:
— Длина не менее 8 символов
— Наличие цифр
— Буквы верхнего и нижнего регистра
"""
# Проверка длины
if len(password) < 8:
return False
# Проверка на наличие цифр
if not any(char.isdigit() for char in password):
return False
# Проверка на верхний регистр
if not any(char.isupper() for char in password):
return False
# Проверка на нижний регистр
if not any(char.islower() for char in password):
return False
return True # Если всё ок, возвращаем TrueТеперь функция is_strong_password содержит все необходимые проверки, поэтому её можно использовать в основном коде программы.
Подробно разбирать рефакторинг в этом примере не будем. Однако код уже можно улучшить: например, дать функциям и переменным понятные названия или вынести отдельные значения и выражения в константы. Попробуйте сделать это сами.

Читайте также:
Преимущества TDD
И при классическом подходе, и при TDD тесты помогают находить ошибки до выпуска продукта. Главное различие — в моменте их создания. У разработки через тестирование при написании тестов до кода есть несколько преимуществ.
Ошибки выявляют раньше. Перед написанием кода разработчик формулирует, как должна работать функция и какие результаты она должна возвращать. Поэтому часть проблем удаётся обнаружить ещё на этапе проектирования, а остальные — сразу после добавления нового фрагмента кода.
Код проще поддерживать. Чтобы функцию было удобно тестировать, её стараются делать небольшой и по возможности независимой от других компонентов. В результате программа состоит из отдельных модулей, которые проще читать, изменять и использовать повторно.
Тесты дополняют документацию. Они на конкретных примерах показывают, что код должен делать. Новому человеку в проекте часто достаточно пробежать тесты, чтобы понять логику, — и это описание не устареет и не разойдётся с кодом, в отличие от текстовой документации, которую вечно забывают обновить.
Проще вносить изменения. Набор тестов позволяет проверить, не нарушило ли обновление кода уже работающую функциональность. Это особенно полезно при рефакторинге — изменении структуры кода без изменения его поведения.
Снижаются затраты на отладку. В начале TDD может замедлить разработку, поскольку для каждого нового сценария нужно сначала подготовить тест. Однако ошибки при использовании этого подхода обнаруживаются вскоре после появления, когда найти их причину проще, чем перед релизом или после запуска продукта.
Требования становятся понятнее. Чтобы написать тест, нужно заранее определить, что должна делать функция, какие данные она получает и как обрабатывает нестандартные ситуации. Это помогает обнаружить неоднозначные требования до начала разработки и сократить количество последующих переделок.
Недостатки TDD
TDD подходит не для всех проектов и этапов разработки. У метода есть несколько ограничений. Разберём основные из них.
Может замедлять создание прототипов. Если команда стартапа за несколько дней собирает первую версию IT-продукта, требования часто меняются, а часть кода вскоре удалят или перепишут. В такой ситуации при выборе TDD разработчики потратят время на создание и поддержку тестов для временной функциональности.
Но это не означает, что от тестирования нужно полностью отказаться. Критически важную логику — например, расчёты, платежи и обработку пользовательских данных — по-прежнему стоит проверять автоматически. Для временных экранов и экспериментальных функций иногда достаточно ручного или интеграционного тестирования.

Читайте также:
Требует навыков написания тестов. Если команда не умеет создавать небольшие и устойчивые проверки, формальное следование TDD может принести больше проблем, чем пользы. Тесты будут ломаться при каждом изменении кода, но при этом не будут находить ошибок в его работе.
В таком случае разработчикам лучше сначала освоить автоматическое тестирование готового кода, а затем постепенно переходить к подходу «сначала тест».
Сложно внедряется в легаси-проекты. В старых IT-продуктах компоненты на уровне часто тесно связаны между собой. Поэтому для написания одного модульного теста может потребоваться серьёзно переработать существующий код.
Безопаснее внедрять TDD постепенно: сначала зафиксировать текущее поведение изменяемого участка с помощью тестов, затем провести рефакторинг и только после этого разрабатывать новую функциональность по принципам TDD.
Частые ошибки в применении TDD
Сам по себе TDD не гарантирует качественного кода. Если применять подход формально, тесты начнут мешать разработке вместо того, чтобы упрощать её. Чаще всего проблемы у разработчиков и QA-инженеров возникают из-за слишком крупных тестов, пропущенного рефакторинга, чрезмерного количества моков и проверок, которые не приносят пользы.
Слишком объёмные тесты. Один тест должен проверять один конкретный сценарий. Если попытаться охватить в нём сразу несколько веток логики и редких ситуаций, тест станет чрезмерно сложным. После изменений в коде его придётся часто переписывать, а найти причину сбоя будет труднее.
Отказ от рефакторинга. Иногда разработчики останавливаются после зелёного этапа — успешного прохождения теста. В результате в коде остаётся дублирование, неудачные названия, лишние зависимости и сложные конструкции. Со временем эти недостатки могут накапливаться и превращаться в технический долг.

Читайте также:
Чрезмерное использование моков и стабов. Моки и стабы помогают изолировать тестируемый модуль от внешних систем, но применять их стоит умеренно. Если подменять почти все зависимости, тесты будут проверять не реальное поведение приложения, а его искусственную модель. Такие проверки могут ломаться после небольших изменений реализации и при этом не замечать ошибки интеграции.
Бесполезные тесты. Некоторые проверки формально увеличивают количество тестов, но не подтверждают корректность работы программы. Например, тест может проверять только наличие метода или тип возвращаемого значения, но не сам результат. Такие тесты бесполезны, так как не привязаны к конкретному сценарию работы, который требуется проверить.
Что дальше
Теперь вы знаете основы разработки через тестирование. Чтобы глубже разобраться в TDD, можно начать с книги Кента Бека «Экстремальное программирование: разработка через тестирование». Автор подробно разбирает подход на примерах на Java и показывает, как тесты влияют не только на проверку кода, но и на его структуру.

После теории стоит перейти к практике на TDD-ката — небольших учебных задачах, которые решают по циклу «красный — зелёный — рефакторинг». Подходящие упражнения можно найти на нескольких площадках:
- Coding Dojo Kata Catalogue — каталог классических задач, среди которых FizzBuzz, римские числа, банковский OCR и подсчёт очков в боулинге;
- Kata-Log — каталог с фильтрами по сложности, теме и формату практики, отдельно собраны задачи для изучения TDD.
Одну и ту же задачу полезно выполнить несколько раз, каждый раз стараясь двигаться меньшими шагами и упрощать решение.
Ну и конечно, можно доработать код с проверкой из этой статьи, добавив к проверкам новые требования. Например, обязать пользователя вводить специальный символ или запретить слишком распространённые пароли.
После этого остаётся последний этап — научиться применять TDD в коде, который взаимодействует с внешними системами: базами данных, API и файловыми хранилищами. Чтобы тесты не зависели от доступности этих сервисов и быстро выполнялись, внешние зависимости заменяют моками, стабами и другими тестовыми объектами.
Больше интересного про код — в нашем телеграм-канале. Подписывайтесь!
