Free

Пусть тесты пишет Claude, а ты только делай review: 1562 строки на практике

Почти все 1562 строки тестов TopicReview написал Claude. Я только делаю review — уже больше двух недель в проде, все последующие коммиты добавляют тесты, ни один их не переписывает. Этот пост о том, почему тесты — идеальная цель делегирования, на что смотреть и что игнорировать в review (включая реально отсутствующий edge case в spec) и о дефолтной конфигурации этого разделения.


Прошлый пост закрыли фразой «119 spec зелёные» для TopicReview. Настоящий следующий вопрос: кто эти тесты писал?

Ответ: Claude написал почти все 1562 строки тестового кода. Я только делаю review. Уже больше двух недель в продакшне, и шаблон сопровождения этих 1562 строк таков — только добавляем новые тесты, никогда не переписываем старые.

Этот пост — о том, почему тесты лучший кандидат на делегирование Claude, на что смотреть и что игнорировать в review, и насколько далеко это разделение реально работает.

Сначала разложим цифры

Тесты TopicReview разбиты на 7 файлов:

spec/services/topic_review_service_spec.rb   760 строк (88 тестов)
spec/requests/topic_reviews_spec.rb          281 строка (32 теста)
spec/requests/review_appeals_spec.rb         152 строки (16 тестов)
spec/requests/review_votes_spec.rb           127 строк
spec/policies/topic_review_policy_spec.rb    109 строк
spec/jobs/close_topic_review_job_spec.rb      71 строка (7 тестов)
spec/models/topic_review_spec.rb              62 строки
───────────────────────────────────────────
                                          1 562 строки

Покрыто четыре типа тестов: service (бизнес-логика), request (controller + интеграция), policy (авторизация Pundit), job (запланированные задачи).

Начальный коммит d162f1e имеет пометку Co-Authored-By: Claude Sonnet 4.6 и вносит 1100+ из этих строк одним махом. Все последующие коммиты по спекам — «Add test for...»; ни одного refactor или rewrite:

00393fc Add test for finalize! with zero votes (expired review)
3f53304 Add test for finalize! with legacy votes missing reasoning
3b185da Update specs to use PROVISIONAL_PENALTY constant

Заплатки, а не переделки. Эта деталь ещё всплывёт.

Почему тесты — лучший кандидат на делегирование

Четыре жёстких причины:

1. Входы и выходы явные. Тест по сути — «дано это состояние → ожидай это поведение». Это сильная сторона Claude: перевести спецификацию в assertion. Бизнес-код иногда требует компромиссов; тесты — почти никогда.

2. Механически × большой объём. Один describe .open! должен покрыть «есть подходящие присяжные / нет / нет topic / уже есть активный review» — четыре context'а, в каждом 2–5 it. Человек на третьем context'е начинает срезать углы. Claude пишет 88-й it с той же тщательностью, что и первый.

3. Очень короткая петля обратной связи. Пишешь тест, запускаешь rspec, за секунды видишь — прошёл или нет. Бизнес-код требует дней реального использования, чтобы проблемы всплыли. Короткая петля = любая ошибка Claude ловится rspec на месте — тебе не нужно караулить.

4. Естественно параллельно. Блоки it независимы, нет скрытых связей, масштабируются тривиально. Сгенерировать десятки изолированных тестов разом — именно то, в чём силён Claude.

На что смотреть в review и что игнорировать

Это ось всего разделения.

Игнорировать:

  • Правильность синтаксиса RSpec → Claude почти никогда не ошибается
  • Качество мока → если нет явного over-mock'а, нормально
  • Эстетику factories → не важно, работает значит работает
  • Единство стиля → если что-то не так, Claude по одной просьбе поправит всё

Смотреть:

  • Реально ли покрыты граничные случаи
  • Описывают ли имена тестов настоящее ожидаемое поведение
  • Нет ли тестов, которые должны быть, но их нет

Последний пункт — настоящая ценность review. Claude покрывает тесты, «которые ему пришли в голову», но те, что не пришли, сами не напишутся. Именно сюда вписывается человеческое review — идти назад от бизнес-правил к недостающему покрытию.

Конкретный пример: что реально ловит review

Открываем начало describe ".open!" в spec/services/topic_review_service_spec.rb:

describe ".open!" do
  context "when there are eligible jurors" do
    # статус review корректен / post under_review / assignments созданы / author уведомлён / jurors уведомлены / двойного open нет
  end
  context "when there are no eligible jurors" do
    # review создаётся, но assignments — нет
  end
  context "when post has no topic" do
    # возвращает nil
  end
end

Выглядит исчерпывающе. Но реальное правило eligible_jurors в модели исключает три группы:

def eligible_jurors
  excluded_ids = [ post.user_id ] + post.reports.pluck(:user_id) + review_votes.where(stage: :initial).pluck(:user_id)
  User.jurors_and_judges.where.not(id: excluded_ids.uniq)
end

Теперь посмотрите тесты — какой тест утверждает, что «автор поста никогда не выбирается в присяжные»?

Перерываешь service_spec.rb и model_spec.rb: нет такого. model_spec.rb тестирует только несколько случаев scope pending_vote_by; eligible_jurors не покрывает напрямую. В service_spec.rb есть лишь комментарий: # Jurors must NOT be the post author — это в setup, не assertion.

Вот что ловит review: три правила исключения (автор / жалобщик / уже-проголосовал-в-initial), ни одно не защищено тестом. Если потом кто-то рефакторит eligible_jurors и случайно уронит post.user_id из списка исключений, все существующие тесты пройдут — а продакшн тихо пустит авторов в их собственную коллегию присяжных.

Claude не ошибся — он протестировал то, что его просили. Он просто сам не спросил: «каждое из этих трёх правил нуждается в покрытии тестом?» Этот вопрос — от правил к покрытию назад — и есть работа review.

(Честно: я сам пропустил это в первом review. Заметил только при втором аудите уже при написании этого поста. То есть и review не «один раз и готово» — но всё равно в 10 раз лучше, чем без review.)

Последующие коммиты подтверждают, что разделение работает на практике

Если «Claude пишет + человек ревьюит» — идеальное разделение, после начального коммита не должно быть новых тест-коммитов. Реальность интереснее — латание дыр без переписывания:

00393fc Add test for finalize! with zero votes (expired review)
3f53304 Add test for finalize! with legacy votes missing reasoning

Первый — регрессионный тест после бага — e8cb2db Default to keep verdict when review expires with zero votes это фикс, 00393fc — парный тест. Тот же паттерн у второго, следом за abaa22e Fix CloseTopicReviewJob failing due to reasoning validation on old votes.

Эти два коммита доказывают две вещи сразу:

  • Review не поймало 100 % случаев — поэтому продакшн выдал два бага
  • Но архитектура тестов выстояла; мы продолжили добавлять тесты без реструктуризации — поэтому коммиты «Add test for...», а не «Rewrite ... spec»

«Достаточно хорошо + можно дальше латать» — куда более реалистичная планка, чем «идеально». Погоня за идеальным review — именно то, что мешает отдать тесты Claude. Принять «достаточно хорошо» — то, что запускает это разделение.

Тесты, которые не стоит полностью отдавать Claude

Не каждому тесту подходит полный handoff:

  • Happy-path E2E — нужен продуктовый взгляд. Claude напишет, но склонен покрыть «технически проходит до конца» и упустить «где реально застревает пользователь»
  • Тесты безопасности — нужен образ мыслей атакующего. Claude консервативен, упускает нестандартные поверхности атаки (инъекция ключевых слов SQL, очень длинные строки, альтернативный unicode)
  • Базовые значения производительности — нужны цифры из реальной среды деплоя. Claude наугад подбирает пороги
  • Большие перекроения fixture / factory — это архитектурный уровень; возвращайся в plan mode, это не то, что ловит review

В этих случаях ведёт человек, Claude ассистирует.

Конфигурация по умолчанию

Превратим это разделение в исполняемый default:

  1. До старта фичи я объясняю бизнес-правила (не соглашения RSpec)
  2. Claude пишет реализацию и тесты
  3. Запускаем тесты. Зелено = дальше. Падает = Claude правит сам
  4. Я делаю review:
    • Не смотрю синтаксис / мок / factory
    • Смотрю покрытие: каждое бизнес-правило защищено хотя бы одним тестом?
    • Допрашиваю граничные случаи: «0 строк / null / конкуренция / нарушение авторизации» — по одному
    • Читаю имена тестов — если по имени не угадаешь, что тестируется, пусть Claude переименует
  5. Баги, пойманные в продакшне, возвращаются как регрессионные тесты — это нормальный износ разделения, а не провал

Напоследок

У программиста меньше ментального ресурса на чтение тестов, чем на чтение кода. Тесты повторяющиеся, механические, изматывающие и нужные. Всё это как раз сильная зона Claude — ему не скучно, не утомительно, он не срезает углы на 50-м it.

Твоя работа не «писать тесты» — а «гарантировать, что каждое бизнес-правило покрыто тестом». Одно — реализация, другое — суждение. Суждение остаётся у тебя; реализация уходит к Claude.

119 spec / 1562 строки выложены одним коммитом и живут больше двух недель без переделки — это не потому что я лучше пишу тесты, а потому что я не написал ни одного. Я делаю лишь то, чего не делает Claude: решаю, какие бизнес-правила заслуживают защиты.