tests-and-ai-coding-tools.html
< BACK 擦り減ったデスク、コードが表示されたノートパソコンを開き、ロンドンの窓からの涼しい光の下でお茶のカップが置かれている。

なぜ私はまたテストを書き始めたのか(AIが私にそうさせた)

2022年初頭にさかのぼり、私は静かな決断を下した。SeahawkMedia経由で進行中のWordPressとNodeプロジェクトの大部分に対するユニットテストを書くのを止めた。派手に発表したわけではない。これについてのブログ記事もない。ただ、やめただけだ。その正当性は十分だと思っていた。月に15~20のクライアントサイトを出荷していて、3人の別の開発者がローテーションで対応していて、私が書いていたテストは誰も読まないドキュメンテーションのように感じられた。手作業のQAが本当のバグを見つけた。テストは演劇だった。

2023年後半に早送りする。GitHub Copilotはエディタに約8ヶ月間入っていた。また、greenfield向けの何かに対してCursorも同時に使い始めていた。速度は本当に驚くべきものだった。しかし何かが起き始めた。私が触れていない場所にバグが現れていた。正しく見えたロジックが、私が確認したことのないエッジケースで間違っていた。そして最悪な部分は、AIがそれが間違っていることに気付いていなかったことだ。それはいつも通りの自信たっぷりなインデントで壊れたコードを書いた。Cursor on the side for anything greenfield. The speed was genuinely remarkable. But something started happening. Bugs were appearing in places I hadn't touched. Logic that looked correct was wrong in edge cases I'd never thought to check. And the worst part — the AI had no idea it was wrong. It wrote the broken code with the same confident indentation it always does.

その時が、私がテストを再び手に取った時だ。

---

テストを放棄した時期(そしてなぜそれが当時意味があったのか)

正直に答えると、特定のタイプのプロジェクトではテストをスキップするのが正解だった。5ページのブロシュアサイトをWordPressで構築しているなら、お問い合わせフォームプラグインのPHPUnitテストを書くのはパフォーマンス的な無駄だ。それは今でも変わらない。was the right call. If you're building a five-page brochure site in WordPress, writing PHPUnit tests for a contact form plugin is theatre. I stand by that.

Seahawkの主力事業は長らくまさにこの手の仕事だった——大量発注、相対的に低い複雑性、明確なスコープ。クライアントがFigmaファイルをくれて、それを構築して、QAをして、デプロイする。フィードバックループは短かった。何か壊れたら数時間以内に分かる。その文脈でテストを書くことは、付箋にラミネート加工を施すのと同じだ。

だが私はその教訓を過度に一般化した。すべてのプロジェクトをブロシュアサイトのように扱い始めた。カスタムWooCommerceチェックアウトフローがあるプロジェクトでも。2023年初めにフランクフルトのクライアント向けに構築したFinTechダッシュボードでも——完全カスタムREST API、JWT認証、3段階のユーザー権限レベル。テストなし。ただ「丁寧な手動QA」。それは傲慢だったし、付けが回ってきた。all projects like brochure sites. Even the ones with custom WooCommerce checkout flows. Even the fintech dashboard we built in early 2023 for a client in Frankfurt — full custom REST API, JWT auth, three different user permission tiers. No tests. Just "careful manual QA." That was arrogant, and it bit us.

フランクフルトのプロジェクトは権限バグを抱えたままリリースされた。編集者レベルのユーザーが特定のフィルター組み合わせで管理者レベルのデータにクエリできるバグだ。彼ら内部のセキュリティレビューを実施して初めて気づいた——本番環境デプロイから6週間後。恥ずかしい。修正可能だ。だが基本的なインテグレーションテストなら、プルリクエストを立てる前に引っかかったはずだ。

---

AI コーディングツールが実際に変えたこと

Copilotとか Cursorとか、その月ホットなモデルについて話すとき、ほとんどの人が見落としていることがある——コードが正しく見える。それが問題だ。looks right. That's the problem.

ジュニア開発者がバグのあるコードを書くと、そのぎこちなさが見えることが多い。奇妙な変数名、「// ここ自信ない」というコメント、明らかに2回コピペされた関数。コードはそれ自体の脆さを表に出す。AIコードはそうではない。スタイル的に一貫していて、命名も適切で、意図的に見える構造をしている。自信は完全に表面的なものだ。// not sure about this, a function that's clearly copy-pasted twice. The code telegraphs its own fragility. AI code doesn't. It's stylistically consistent, well-named, and structured in a way that reads as intentional. The confidence is entirely cosmetic.

スタンフォード大学のHuman-Computer Interactionグループの研究によると、AI助言ツールを使う開発者は、生成されたコードを最初の一読で過度に信頼する傾向にあるという。それは私の経験とも一致する。Copilotが書いた40行の関数をさっと見て、「まあ、自分が書いたものと大体同じだ」と思って進めていた。時には大丈夫だった。時には微妙に私の実際の要件を誤解していた。 have flagged that developers using AI assistants tend to over-trust generated code on first read. That tracks with my own experience. I would glance at a 40-line function Copilot had written, think "yeah, that's basically what I'd have written," and move on. Sometimes it was fine. Sometimes it had silently misunderstood what I actually needed.

何度も引っかかった具体的な失敗モード——AIが予想する理由のなかった境界ケースの条件分岐だ。幸福なパスは完璧に扱うのに、nullの入力、空の配列、非標準の日付フォーマットで静かに失敗する関数を書く。自分でコードを書いていれば30秒で考えられたことだ。入力しながら考えていただろうから。conditional logic around edge cases the AI had no reason to anticipate. It would write a function that handled the happy path perfectly and then quietly fail on null inputs, empty arrays, or non-standard date formats. Things that would have taken me thirty seconds to think about if I'd written the code myself, because I'd have been thinking as I typed.

スピードの罠

ここには本当の生産性の罠がある。AIはあなたを速くする。速さは良く感じる。より速く出荷し始め、速度が品質の証拠のように感じられるため、レビューはより注意深くなくなる。そうではない。言語モデルにプロンプトを与えるとき、スピードと正確性は相関していない。

昨年9月、AIの支援なしでは実現できなかったよりも、大体40%多くの機能をクライアントプロジェクトに組み込んだ。そのプロジェクトは、ここ2年間で出荷した他のどのプロジェクトよりも、ローンチ後のバグが多かった。致命的なバグではない。ただ厄介なバグだ。クライアントの信頼を損なわせるようなやつだ。

---

テストが今(AIがループに入った場合)異なる理由

テストに戻ったとき、昔のワークフローに戻ったわけではない。まずテストを書き、その次に実装を行い、その後AIによるコードレビューを受ける — それが今落ち着いたループだ。

興味深いのは、AIは実は、アプリケーションロジック執筆ではないときのように、テスト執筆で非常に優れているということだ。Copilotに定義されたよい関数シグネチャを与えて、テストスイートを生成するよう依頼すれば、手動で書くのに20分かかるであろうエッジケースカバレッジを生成する。タスクが具体的に「これがどのように壊れるのかを見つけること」であるとき、それは不幸なパスをよく想像する。AI is actually excellent at writing tests, in a way it isn't always excellent at writing application logic. Give Copilot a well-defined function signature and ask it to generate a test suite and it'll produce edge-case coverage I'd have taken twenty minutes to write manually. It imagines unhappy paths well when the task is specifically "find ways this can break."

だからある種、逆にした。テストスペックを書く。AIがテストケースを埋める。その後AIが実装を書く。その後、コードを冷たく読むのではなく、それらのテストのレンズを通して実装を読む。through the lens of those tests, rather than just reading the code cold.

これは純粋なノリでコーディングするより遅い。ただし、テストを含むすべてを手動で書く昔のワークフローより速い。そしてフランクフルト以来、ゼロの権限バグを出荷した。

実際に使っているツール

  • JavaScript や TypeScript であれば [Vitest](https://vitest.dev) を使う。去年 Jest に完全に置き換わった — 設定がより合理的で、ウォッチモードが高速だ。 for anything JavaScript or TypeScript. Replaced Jest for me entirely last year — the config is saner and the watch mode is quick.
  • WordPress とカスタム PHP の仕事は PHPUnit を使い続けている。これに取って代わるものはまだない。 still, for WordPress and custom PHP work. Nothing has replaced it.
  • Cursor の「この関数をテストする」ショートカット — 本当に、使ったことのあるエディタの中で最も便利な機能の一つだ。 — genuinely one of the most useful single features in any editor I've used.
  • CI には GitHub Actions を使う。main へのプッシュごとにテストが実行される。ほとんどのプロジェクトで約 90 秒かかる。 for CI. Tests run on every push to main. Takes about 90 seconds on most projects.

---

テストに対する議論(公正に論じた場合)

この立場に真摯に向き合いたい。なぜなら、ほぼ 2 年間、私自身がこの立場にいたからだ。

実際の議論は「テストは無用だ」ではない。「テストにはコストがあり、多くのプロジェクトはそのコストを正当化しない」ということだ。テストスイートの作成と保守には時間がかかる。キャンペーン用マイクロサイト、マーケティングランディングページ、ハッカソン向けプロトタイプのような寿命の短いプロジェクトでは、その時間投資は何も返してこない。テストが役に立つ前に、プロジェクトが終わるだろう。

もっと微妙な点もある — 不十分なテストは、テストがないよりも悪い。テストが同義反復になっているために合格するテストスイート(本質的に、関数が返すべき値を返しているかをテストしているだけ)は、根拠のない自信を生む。代理店でこれを見かけた。開発者が、実際に何を検証しているのかを誰も問い直さないから、いつも合格するテストを書いているのだ。bad tests are worse than no tests. A test suite that passes because the tests are tautological (you're essentially testing that your function returns what you told it to return) gives you false confidence. I've seen this at agencies. Developers writing tests that always pass because nobody challenged what they were actually verifying.

Martin Fowler はこれについてよく書いている — カバレッジのパーセンテージはテストの品質の尺度ではない。90% のカバレッジ数字は、完全に空っぽのスイートを隠蔽することができる。 — coverage percentages are not a measure of test quality. A 90% coverage number can mask a completely hollow suite.

つまり、すべてをテストするな。プロフェッショナルに見えるからテストするな。負荷を支える論理を特定して、それが壊れると高くつく場合にテストしろ。

---

今、私がテストすること(そしてテストしないこと)

ここに、過去8~9ヶ月間で辿り着いた実際の判断がある:

私はテストする:

  1. お金、権限、またはデータ変換を扱う関数すべて
  2. 単純なCRUDパススルーではないAPIエンドポイントすべて
  3. クライアントが書面で正確な動作を指定したカスタムビジネスロジック
  4. AIが書いたが、1行ずつ完全には読まなかったすべてのコード

私はテストしない:

  • UIレンダリング(スナップショットテストは9年間で一度も役に立ったことがない。一度も。)
  • 外部の動作が自分のコントロール下にないサードパーティAPIラッパー
  • 一度だけ実行して削除される使い捨てスクリプト
  • 標準的なWordPressフック(ただし何か変わったことをしている場合は除く)

それだけだ。大げさな哲学はない。実際に失敗を経験した場所に基づいたリストにすぎない。

---

実際に機能するワークフロー

参加しているSlackコミュニティで何人かに聞かれたから、実際の流れを紹介する。

  1. ファイルの先頭に簡潔な仕様コメントを書く。このモジュールが何をするのか、何をしないのか、すでに知っているエッジケースについて。
  2. 実装を書く前に、Cursorにそのコメントからテストケースを生成するよう依頼する。
  3. テストケースを確認する。つまらないやつは削除する。AIが見落としたものを追加する。
  4. CopilotやCursorに実装を書かせる。
  5. テストを実行する。失敗する。実装を修正する(テストではなく)。
  6. プッシュする前にdiffを読む — AI支援コードでも人間による確認が必要だ。

ステップ6は譲歩の余地がない。過去4ヶ月間、diffをじっくり読んでからプッシュするだけで、本当に悪いバグを3つ捕捉している。難しいことではない。ただ読むだけだ。

Kent Beckによる当初のTDDの枠組みは、100%のカバレッジや完璧な方法論についてのものではなかった。それはミスが複合される前に捕捉するのに十分な速度のフィードバックループを構築することだった。その考え — 高速フィードバックループ — は2003年のときより今の方がはるかに関連性がある。なぜならAIは私がこれまで雇ってきた開発者より速くミスを犯すからだ。 was never about 100% coverage or perfect methodology. It was about building a feedback loop fast enough to catch mistakes before they compound. That idea — fast feedback loops — is more relevant now than it was in 2003. Because the AI makes mistakes faster than any developer I've ever hired.

---

FAQ

納期スピードは遅くなるか?

複雑なプロジェクトでは約10~15%遅くなる。シンプルなプロジェクトではほぼ変わらない — AIがテストを非常に迅速に生成するため、オーバーヘッドは最小限だ。バグのローンチ後の修正が実際のお金を失う可能性があるプロジェクト(そしてほとんどの実際のお金が関わるプロジェクトがそれに当てはまる)では、その15%は百倍の価値がある。

TypeScriptはどうか。強い型付けによって、多くのテストが不要になるのではないか。

部分的にはそうだ。TypeScriptはコンパイル時に、以前はテストが必要だった一連のエラーをキャッチする。しかし型はビジネスロジックをテストしない。割引計算関数が卸売顧客に対して正しいルールを適用しているかどうかを検証しない。それはあなたの責任だ。

テストを書いていないジュニアデベロッパーがAIコーディングツールを使うべきか。

いいえ。強い意見だ。テストなしでCopilotを使うジュニアデベロッパーは、本質的にオートパイロットで飛行機を操縦していることと同じだ。オートパイロットの仕組みやマニュアルで着陸する方法を理解せずに。AIは上級レベルに見えるコードを生成し、ジュニアはどの部分を信用できないかわからず、結局本番環境でインシデントが発生する。テストは少なくとも、受け入れているアウトプットを検証するためのメカニズムを与えてくれる。

そもそも、なぜテストをやめたのか。正直に答えてほしい。

バーンアウト、それが一部だ。そしてすべてのプロジェクトが本当にシンプルで、テストが本当に役に立たない時期があった。間違いは、プロジェクトの複雑性が変わったときに気づかず、それに応じて調整しなかったことだ。本当の教訓は「常にテストする」でも「テストしない」でもなく、あるプロジェクトがどのカテゴリーに当てはまるかを知ることだ。

---

テストを書くことは保護のように感じられなかった。事務作業のように感じられた。AIはそれを変えた。AIが悪いからではなく(AIは私を実質的に速くしてくれた)、むしろ確信に満ちた、よくフォーマットされた、もっともらしい間違いという新しいカテゴリーが導入されたからだ。私がコードを読むという従来の方法では捕捉できないような。テストはAIのためではない。私のためだ。モデルが何を渡そうが、それを受け入れる前に、コードが実際に何をする必要があるのかについて考える強制メカニズムだ。

2年前にそのように考えていればよかったと思う。

< BACK