TDD: разработка через тестирование
TDD — дисциплина разработки, при которой тест пишется до кода. Цикл Red → Green → Refactor создаёт код, который по определению тестируем, и документацию, которая всегда актуальна.
Почему это важно: Код без тестов — это бомба замедленного действия. Код, к которому тесты добавлены потом — часто плохо тестируем (жёсткие зависимости, side effects). TDD гарантирует, что каждая строка написана с учётом тестируемости.
Главная идея
Red → Green → Refactor. Red: напишите падающий тест, описывающий желаемое поведение. Green: напишите минимум кода, чтобы тест прошёл. Refactor: улучшите код, убедившись, что тесты остаются зелёными.
Как это выглядит на практике
- Задача: реализовать расчёт скидки — 10% для заказов > 5000 руб
- RED: пишем тест: expect(calculator.discount(6000)).to eq(600)
- Тест падает: NoMethodError — класса DiscountCalculator ещё нет
- GREEN: создаём класс с минимальной реализацией: amount > 5000 ? amount * 0.1 : 0
- Тест зелёный. Добавляем ещё тесты: 0 для 3000, граничный случай для 5000
- REFACTOR: выносим порог и процент в константы, код чище, тесты по-прежнему зелёные
Примеры кода
TDD-цикл: расчёт скидки
# === RED: пишем тест ДО кода ===
# spec/services/discount_calculator_spec.rb
RSpec.describe DiscountCalculator do
subject(:calculator) { described_class.new }
describe '#calculate' do
context 'когда сумма > 5000' do
it 'возвращает 10% скидку' do
expect(calculator.calculate(6000)).to eq(600)
end
end
context 'когда сумма <= 5000' do
it 'возвращает 0' do
expect(calculator.calculate(3000)).to eq(0)
end
end
context 'граничный случай: ровно 5000' do
it 'не даёт скидку' do
expect(calculator.calculate(5000)).to eq(0)
end
end
context 'когда сумма > 20000 (VIP)' do
it 'возвращает 20% скидку' do
expect(calculator.calculate(25000)).to eq(5000)
end
end
end
end
# Запуск: все 4 теста красные (класса нет)
# === GREEN: минимальная реализация ===
# app/services/discount_calculator.rb
class DiscountCalculator
TIERS = [
{ threshold: 20_000, rate: 0.20 },
{ threshold: 5_000, rate: 0.10 }
].freeze
def calculate(amount)
tier = TIERS.find { |t| amount > t[:threshold] }
tier ? (amount * tier[:rate]).round : 0
end
end
# Запуск: все 4 теста зелёные
# === REFACTOR ===
# Код уже чистый. Добавим новый тир?
# 1. Пишем тест для нового тира
# 2. Добавляем запись в TIERS
# 3. Все старые тесты по-прежнему зелёные
TDD для Service Object с зависимостями
# Тест пишем первым — определяем интерфейс
RSpec.describe PlaceOrder do
let(:repo) { instance_double('OrderRepository') }
let(:payment) { instance_double('PaymentGateway') }
let(:notifier) { instance_double('Notifier') }
subject(:service) do
described_class.new(
order_repo: repo,
payment_gateway: payment,
notifier: notifier
)
end
describe '#call' do
let(:items) { [{ product_id: 1, price: 1000, qty: 2 }] }
it 'списывает оплату, сохраняет заказ и уведомляет' do
allow(payment).to receive(:charge)
allow(repo).to receive(:save)
allow(notifier).to receive(:order_placed)
service.call(customer_id: 42, items: items)
expect(payment).to have_received(:charge).with(2000, customer_id: 42)
expect(repo).to have_received(:save)
expect(notifier).to have_received(:order_placed)
end
context 'когда оплата отклонена' do
it 'не сохраняет заказ' do
allow(payment).to receive(:charge)
.and_raise(PaymentDeclined)
expect { service.call(customer_id: 42, items: items) }
.to raise_error(PaymentDeclined)
expect(repo).not_to have_received(:save)
end
end
end
end
# Тест написан ДО реализации PlaceOrder.
# Он определил: конструктор с 3 зависимостями,
# метод call с customer_id + items,
# порядок операций: charge → save → notify
Что происходит под капотом
- Red → Green → Refactor: маленькие циклы по 2–5 минут, не по часу
- Тест первым = дизайн API первым: вы определяете, как будете ИСПОЛЬЗОВАТЬ класс, до его написания
- TDD заставляет инжектить зависимости — иначе невозможно тестировать в изоляции
- 100% coverage не цель TDD. Цель — уверенность в коде и чистый дизайн
- TDD лучше всего работает для бизнес-логики; для UI, миграций и интеграций — менее эффективен
Типичные ошибки и заблуждения
- «TDD замедляет разработку» — в краткосрочной перспективе да (+15–30% времени), но окупается в 3–5 раз на поддержке и дебаге
- «TDD = 100% test coverage» — coverage — побочный эффект, не цель. Важна уверенность, а не число
- «Сначала нужно написать весь код, потом тесты» — при этом подходе тесты часто не пишутся вообще, а если пишутся — подстраиваются под реализацию
Ключевые выводы
- Red → Green → Refactor: маленькие шаги, каждый тест добавляет одно поведение
- Тест первым = дизайн интерфейса первым: вы решаете, как класс будет использоваться
- TDD естественно приводит к инъекции зависимостей и SOLID
Термины урока
Связь с работой backend-разработчика
TDD — не религия, а дисциплина. Используйте его для бизнес-логики (сервисы, калькуляторы, валидаторы), где он даёт максимальную отдачу. Для простых CRUD-контроллеров и миграций — пишите тесты после, это нормально.
Мини-разбор реальной ситуации
Платёжный сервис: команда из 4 разработчиков перешла на TDD для модуля расчёта комиссий (50+ правил). За 3 месяца: количество багов в расчётах упало с 8 в месяц до 0, время добавления нового правила сократилось с 2 дней до 3 часов (написал тест → написал правило → готово). Команда отмечает, что тесты стали лучшей документацией — новый разработчик понимает бизнес-логику, читая spec-файлы.
Что запомнить
- Red → Green → Refactor: маленькие циклы по 2–5 минут
- Тест первым определяет интерфейс класса — как он будет использоваться
- TDD для бизнес-логики, тесты-после для CRUD и инфраструктуры
Итог
TDD — дисциплина, при которой тесты пишутся до кода. Цикл Red → Green → Refactor создаёт код, который тестируем по определению, и тесты, которые служат живой документацией. Лучше всего работает для сложной бизнес-логики.
Комментарии к уроку
Войдите, чтобы оставить комментарий.
Пока нет комментариев — будьте первым.