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+ із цих рядків за один захід. Усі подальші коміти по spec — «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: вирішую, які бізнес-правила варто захищати.