Free

テストは Claude に書かせ、あなたは review だけ——1,562 行の分業実戦

TopicReview の 1,562 行のテストはほぼすべて Claude が書いた。私は review だけ——本番投入から 2 週間以上、新規テスト追加のみで既存テストの書き直しはゼロ。この記事では、なぜテストが Claude に任せるべき最上位の仕事か、review で見るもの見ないもの(実際の spec に潜む見落とし境界を含む)、そしてこの分業のデフォルト構成について。


前回の記事の結びで TopicReview の「119 spec 全部グリーン」と書いた。読者が本当に問うべき次の質問:これらのテストは誰が書いたのか?

答え:Claude がほぼすべての 1,562 行のテストコードを書いた。私は review のみ。本番投入から 2 週間以上が経ち、この 1,562 行のメンテナンスは新規テストの追加のみ、既存テストの書き直しはゼロ

この記事では、なぜテストは 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 行

4 種類のテストをカバー:service(業務ロジック)、request(controller + integration)、policy(Pundit 権限)、job(スケジュール実行)。

初期コミット d162f1eCo-Authored-By: Claude Sonnet 4.6 付きで、うち 1,100 行以上を一度に書いた。その後の 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

穴埋めであって、やり直しではない。この細部が後で効いてくる。

なぜテストは Claude に書かせるのが正解か

硬い理由が 4 つ:

1. 入力と出力が明確。テストの本質は「与えられた状態 → 期待される挙動」。これは Claude が最も得意とする翻訳——仕様をアサーションに変換するだけ。業務ロジックは時に熟慮が必要だが、テストはそうでもない。

2. 機械的 × 大量describe .open! の一つの下に「陪審員あり / 陪審員なし / topic なし / すでに active な review がある」の 4 context、各 context に 2〜5 の it block。人間は 3 つめの context で省略し始める。Claude は 88 個目の it を 1 個目と同じ丁寧さで書く。

3. フィードバックループが極端に短い。テストを書いて rspec を走らせると、通ったか通らないかがすぐ分かる。業務コードは実運用で数日使って初めて問題が浮かぶ。ループが短いということは、Claude のミスが rspec にその場で拾われる——見張る必要がない。

4. 自然に並列化できるit block は互いに独立し、隠れた依存がない。Claude が数十の独立テストを一度に書くのはまさに得意領域。

Review 時に見るもの、見ないもの

ここが分業全体の肝。

見ない

  • RSpec の文法が合っているか → Claude はほぼ間違えない
  • mock の書き方 → あからさまな over-mock でなければ通常 OK
  • Factory のエレガンス → 重要ではない、動けばよい
  • テストスタイルの統一 → 問題があれば Claude に一斉修正を頼めばよい

見る

  • 境界ケースが本当にカバーされているか
  • テスト名が実際の期待挙動を表しているか
  • 書かれるべきなのに書かれていないテストがあるか

最後の一つが review の真の価値。Claude は「思いつくテスト」は書くが、思いつかないテストは自発的には補完しない。human review のポジションはまさにそこ——業務ルールから逆算してカバレッジの欠落を探す

具体例:review が実際に見つけたもの

spec/services/topic_review_service_spec.rb の冒頭にある describe ".open!" を開く:

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

包括的に見える。だが model の eligible_jurors の実ルールは 3 種類を除外する:

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.rbmodel_spec.rb を全部見ても、ない。model_spec.rbpending_vote_by スコープの数ケースしか扱っていない、eligible_jurors 自体は未テスト。service_spec.rb にはコメントが一行あるだけ:# Jurors must NOT be the post author——setup のコメント、断言ではない。

これが review の見つけるべき欠落:3 種類の除外ルール(原作者 / 通報者 / 初審で投票済み)、どれもテストで保護されていない。後日誰かが eligible_jurors をリファクタリングし、うっかり post.user_id を除外リストから落としても、既存のテストはすべて通る——本番では、原作者が自分の陪審に座ることになる。

Claude は間違っていない。要求された通りのテストを書いた。ただ「この 3 つのルールそれぞれにテスト保護が要りますか?」と自発的に尋ねなかった。この問い——ルールからカバレッジへの逆算——こそ review の仕事。

(正直に言うと、最初の review でも私はこれを見落とした。この記事を書くための 2 回目の audit で気づいた。review も一発で完全にはならない——でもしない場合より 10 倍いい。)

その後のコミットが分業の現実性を裏付ける

「Claude が書き + human が review」が完璧な分業なら、初期コミット以降に新しいテストコミットは出ないはず。実際はもっと興味深い——穴埋めはあるが書き直しはない

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

最初のは bug 修正後の regression test——e8cb2db Default to keep verdict when review expires with zero votes が fix、00393fc がペアになる追加テスト。2 つ目は abaa22e Fix CloseTopicReviewJob failing due to reasoning validation on old votes を追う形。

この 2 つのコミットが同時に証明するのは:

  • review は 100% のケースを捕まえられていない——だから本番で 2 件の bug が露出した
  • しかしテスト基盤は安定しており、追加テストを入れ続けられた——だからコミット名は「Add test for...」であって「Rewrite ... spec」ではない

「十分に良い + 継続的に補修できる」——「完璧」よりはるかに現実的な基準。完璧な review を追うと、テストを Claude に渡せなくなる。「十分に良い」を受け入れることで分業が動き始める

Claude に独立させて書かせるべきではないテスト

すべてのテストが完全な委譲に向くわけではない:

  • E2E のハッピーパス——プロダクト視点が必要。Claude は書けるが「技術的に完走する」だけを拾いがちで、「ユーザーが実際詰まるところ」を取り逃す
  • セキュリティテスト——攻撃者マインドが必要。Claude は保守的で、非標準な攻撃面(SQL キーワード注入、過大な文字列、代替 unicode など)を逃しやすい
  • 性能ベースライン——実環境の数値が必要。Claude は閾値を当てずっぽうで書きがち
  • 影響範囲の広い fixture / factory の改造——これは設計レベルの改変で、review で拾うべきではなく plan mode へ戻る案件

こうしたケースでは人間が主導し、Claude が補助する。

デフォルト設定

この分業を実行可能な default に落とし込む:

  1. 機能を始める前、私は業務ルールを説明する(RSpec の規約ではない)
  2. Claude が実装とテストを書く
  3. テストを走らせ、通過 = 続行、失敗 = Claude が自分で直す
  4. 私が review:
    • 文法/mock/factory は見ない
    • カバレッジを見る:各業務ルールは少なくとも一つのテストに守られているか
    • エッジケースを問い詰める:「0 件/空値/並行/権限違反」の軸で順に
    • テスト名を読む——名前から挙動を当てられなければ、Claude にリネームさせる
  5. 本番で見つかった bug は regression test を追加して戻す——これは分業の正常な摩耗であって失敗ではない

結び

プログラマがテストを読むときの心理リソースはコードを読むときより少ない。テストは反復的で、機械的で、消耗するが必須。そのすべてが Claude の強みと重なる——飽きない、疲れない、50 個目の it で手を抜かない。

あなたの仕事は「テストを書くこと」ではなく「すべての業務ルールがテストに守られていることを保証すること」。一方は実装、もう一方は判断——判断は自分に残し、実装は Claude に渡す。

119 spec / 1,562 行が一度のリリースで上がり、2 週間以上ノーリワークで回っている——私が上手く書いたからではなく、私はまったく書いていないから。私が Claude にできない一点だけをやった:どの業務ルールが保護に値するかを判断した。