Средний

TDD: разработка через тестирование

Урок 5 из 6 в курсе Архитектурные паттерны и подходы к проектированию

Содержание курса (5/6)

TDD: разработка через тестирование

TDD — дисциплина разработки, при которой тест пишется до кода. Цикл Red → Green → Refactor создаёт код, который по определению тестируем, и документацию, которая всегда актуальна.

Почему это важно: Код без тестов — это бомба замедленного действия. Код, к которому тесты добавлены потом — часто плохо тестируем (жёсткие зависимости, side effects). TDD гарантирует, что каждая строка написана с учётом тестируемости.

Главная идея

Red → Green → Refactor. Red: напишите падающий тест, описывающий желаемое поведение. Green: напишите минимум кода, чтобы тест прошёл. Refactor: улучшите код, убедившись, что тесты остаются зелёными.

Как это выглядит на практике

  1. Задача: реализовать расчёт скидки — 10% для заказов > 5000 руб
  2. RED: пишем тест: expect(calculator.discount(6000)).to eq(600)
  3. Тест падает: NoMethodError — класса DiscountCalculator ещё нет
  4. GREEN: создаём класс с минимальной реализацией: amount > 5000 ? amount * 0.1 : 0
  5. Тест зелёный. Добавляем ещё тесты: 0 для 3000, граничный случай для 5000
  6. 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

Термины урока

{:term=>"Red", :definition=>"Первый шаг TDD: написать тест, который описывает желаемое поведение и падает (потому что кода ещё нет)"}
{:term=>"Green", :definition=>"Второй шаг TDD: написать минимальный код, чтобы тест прошёл. Никаких оптимизаций"}
{:term=>"Refactor", :definition=>"Третий шаг TDD: улучшить дизайн кода, сохраняя все тесты зелёными"}

Связь с работой backend-разработчика

TDD — не религия, а дисциплина. Используйте его для бизнес-логики (сервисы, калькуляторы, валидаторы), где он даёт максимальную отдачу. Для простых CRUD-контроллеров и миграций — пишите тесты после, это нормально.

Мини-разбор реальной ситуации

Платёжный сервис: команда из 4 разработчиков перешла на TDD для модуля расчёта комиссий (50+ правил). За 3 месяца: количество багов в расчётах упало с 8 в месяц до 0, время добавления нового правила сократилось с 2 дней до 3 часов (написал тест → написал правило → готово). Команда отмечает, что тесты стали лучшей документацией — новый разработчик понимает бизнес-логику, читая spec-файлы.

Что запомнить

  • Red → Green → Refactor: маленькие циклы по 2–5 минут
  • Тест первым определяет интерфейс класса — как он будет использоваться
  • TDD для бизнес-логики, тесты-после для CRUD и инфраструктуры

Итог

TDD — дисциплина, при которой тесты пишутся до кода. Цикл Red → Green → Refactor создаёт код, который тестируем по определению, и тесты, которые служат живой документацией. Лучше всего работает для сложной бизнес-логики.

Комментарии к уроку

Войдите, чтобы оставить комментарий.

Пока нет комментариев — будьте первым.