2022年初頭にさかのぼると、静かな決断をした。Seahawkを通じてくるWordPressとNodeのプロジェクトのほとんどについて、ユニットテストの作成をやめた。大々的にではなく。ブログ記事もない。ただやめたのだ。その正当性は堅牢だと思っていた。月に15〜20のクライアントサイトを出荷していて、3人の開発者がローテーションで動いていて、書いていたテストは誰も読まないドキュメントのようなものだった。手動QAが本物のバグをキャッチしていた。テストはただの演出だった。
2023年末に話を進めると、GitHub Copilotが約8ヶ月間、自分のエディタに入っていた。また、greenfield関連で Cursor も使い始めていた。速度は本当に目覚ましかった。しかし何かが起き始めた。触れていない場所にバグが現れた。正しく見えるロジックは、確認したことのないエッジケースで間違っていた。そして最悪なことに、AIはそれが間違っていることに気づいていなかった。壊れたコードを、いつもと同じ自信を持ったインデント付きで書いた。
その時が、私がテストを再び手に取った時だ。
---
テストを放棄した時期(そしてなぜそれが当時意味があったのか)
正直に答えると、特定のタイプのプロジェクトではテストをスキップするのが正解だった。5ページのブロシュアサイトをWordPressで構築しているなら、お問い合わせフォームプラグインのPHPUnitテストを書くのはパフォーマンス的な無駄だ。それは今でも変わらない。
Seahawkの長年の主力事業は、まさにそういった仕事だった。高ボリューム、比較的低い複雑性、よく定義されたスコープ。クライアントがFigmaファイルを渡す、君がそれを構築する、QAをする、出荷する。フィードバックループは短かった。何かが壊れれば、数時間以内に知ることになった。そのコンテキストでテストを書くことは、付箋ノートをラミネート加工する開発者版のようなものだった。
しかし自分はその教訓を過度に一般化してしまった。すべてのプロジェクトをブロシュアサイトのように扱い始めた。カスタムWooCommerceチェックアウトフローがあるものでさえ。2023年初頭にフランクフルトのクライアント向けに構築したfintech ダッシュボードでさえ。フルカスタムREST API、JWT認証、3つの異なるユーザー権限レベル。テストなし。ただ「注意深い手動QA」。それは傲慢だったし、自分たちに降りかかってきた。
フランクフルトのプロジェクトは権限バグを抱えたままリリースされた。編集者レベルのユーザーが特定のフィルター組み合わせで管理者レベルのデータにクエリできるバグだ。彼ら内部のセキュリティレビューを実施して初めて気づいた——本番環境デプロイから6週間後。恥ずかしい。修正可能だ。だが基本的なインテグレーションテストなら、プルリクエストを立てる前に引っかかったはずだ。
---
AI コーディングツールが実際に変えたこと
Copilotとか Cursorとか、その月ホットなモデルについて話すとき、ほとんどの人が見落としていることがある——コードが正しく見える。それが問題だ。
ジュニア開発者がバグのあるコードを書くと、そのぎこちなさが見えることが多い。奇妙な変数名、「// ここ自信ない」というコメント、明らかに2回コピペされた関数。コードはそれ自体の脆さを表に出す。AIコードはそうではない。スタイル的に一貫していて、命名も適切で、意図的に見える構造をしている。自信は完全に表面的なものだ。
スタンフォード大学のHuman-Computer Interactionグループの研究によると、AI助言ツールを使う開発者は、生成されたコードを最初の一読で過度に信頼する傾向にあるという。それは私の経験とも一致する。Copilotが書いた40行の関数をさっと見て、「まあ、自分が書いたものと大体同じだ」と思って進めていた。時には大丈夫だった。時には微妙に私の実際の要件を誤解していた。
何度も引っかかった具体的な失敗モード——AIが予想する理由のなかった境界ケースの条件分岐だ。幸福なパスは完璧に扱うのに、nullの入力、空の配列、非標準の日付フォーマットで静かに失敗する関数を書く。自分でコードを書いていれば30秒で考えられたことだ。入力しながら考えていただろうから。
スピードの罠
ここには本当の生産性の罠がある。AIはあなたを速くする。速さは良く感じる。より速く出荷し始め、速度が品質の証拠のように感じられるため、レビューはより注意深くなくなる。そうではない。言語モデルにプロンプトを与えるとき、スピードと正確性は相関していない。
昨年9月、AIの支援なしでは実現できなかったよりも、大体40%多くの機能をクライアントプロジェクトに組み込んだ。そのプロジェクトは、ここ2年間で出荷した他のどのプロジェクトよりも、ローンチ後のバグが多かった。致命的なバグではない。ただ厄介なバグだ。クライアントの信頼を損なわせるようなやつだ。
---
テストが今(AIがループに入った場合)異なる理由
テストに戻ってきたとき、古いワークフローに戻ったのではなかった。テストを最初に書く、次に実装する、次にAI補助のコードレビュー、そのループが今、自分が落ち着いたところだ。
興味深いのは、AIは実は、アプリケーションロジック執筆ではないときのように、テスト執筆で非常に優れているということだ。Copilotに定義されたよい関数シグネチャを与えて、テストスイートを生成するよう依頼すれば、手動で書くのに20分かかるであろうエッジケースカバレッジを生成する。タスクが具体的に「これがどのように壊れるのかを見つけること」であるとき、それは不幸なパスをよく想像する。
だからある種、逆にした。テストスペックを書く。AIがテストケースを埋める。その後AIが実装を書く。その後、コードを冷たく読むのではなく、それらのテストのレンズを通して実装を読む。
これは純粋なノリでコーディングするより遅い。ただし、テストを含むすべてを手動で書く昔のワークフローより速い。そしてフランクフルト以来、ゼロの権限バグを出荷した。
実際に使っているツール
- JavaScriptまたはTypeScriptのものには[Vitest](https://vitest.dev)を使う。昨年Jestを完全に置き換えた。設定はより合理的で、ウォッチモードは高速だ。
- WordPress とカスタム PHP の仕事は PHPUnit を使い続けている。これに取って代わるものはまだない。
- Cursorの「この関数をテストする」ショートカット。使ったことのあるどのエディタの単一機能よりも、本当に有用なものだ。
- CI には GitHub Actions を使う。main へのプッシュごとにテストが実行される。ほとんどのプロジェクトで約 90 秒かかる。
---
テストに対する議論(公正に論じた場合)
この立場に真摯に向き合いたい。なぜなら、ほぼ 2 年間、私自身がこの立場にいたからだ。
本当の議論は「テストは無駄」ではない。「テストにはコストがあり、多くのプロジェクトはそのコストを正当化しない」ということだ。テストスイートを書いて保守することは時間がかかる。寿命の短いプロジェクト、キャンペーンマイクロサイト、マーケティングランディングページ、ハッカソンプロトタイプでは、その時間投資はゼロリターンだ。プロジェクトがテストに救われる前に終わってしまう。
もっと微妙な点もある — 不十分なテストは、テストがないよりも悪い。テストが同義反復になっているために合格するテストスイート(本質的に、関数が返すべき値を返しているかをテストしているだけ)は、根拠のない自信を生む。代理店でこれを見かけた。開発者が、実際に何を検証しているのかを誰も問い直さないから、いつも合格するテストを書いているのだ。
Martin Fowlerはこれについてよく書いている。カバレッジのパーセンテージはテスト品質の測定ではない。90%のカバレッジ数は、完全に空洞なスイートを隠すことができる。
つまり、すべてをテストするな。プロフェッショナルに見えるからテストするな。負荷を支える論理を特定して、それが壊れると高くつく場合にテストしろ。
---
今、私がテストすること(そしてテストしないこと)
ここに、過去8~9ヶ月間で辿り着いた実際の判断がある:
私はテストする:
- お金、権限、またはデータ変換を扱う関数すべて
- 単純なCRUDパススルーではないAPIエンドポイントすべて
- クライアントが書面で正確な動作を指定したカスタムビジネスロジック
- AIが書いたが、1行ずつ完全には読まなかったすべてのコード
私はテストしない:
- UIレンダリング(スナップショットテストは9年間で一度も役に立ったことがない。一度も。)
- 外部の動作が自分のコントロール下にないサードパーティAPIラッパー
- 一度だけ実行して削除される使い捨てスクリプト
- 標準的なWordPressフック(ただし何か変わったことをしている場合は除く)
それだけだ。大げさな哲学はない。実際に失敗を経験した場所に基づいたリストにすぎない。
---
実際に機能するワークフロー
参加しているSlackコミュニティで何人かに聞かれたから、実際の流れを紹介する。
- ファイルの最上部に簡潔な仕様コメントを記述する。このモジュールが何をするのか、何をしないのか、既に認識している端のケースについて。
- 実装を書く前に、Cursorにそのコメントからテストケースを生成するよう依頼する。
- テストケースを確認する。つまらないやつは削除する。AIが見落としたものを追加する。
- CopilotやCursorに実装を書かせる。
- テストを実行する。失敗する。実装を修正する(テストではなく)。
- プッシュする前に差分を読む。AI支援のコードであっても、人間による確認が必要だ。
ステップ6は譲歩の余地がない。過去4ヶ月間、diffをじっくり読んでからプッシュするだけで、本当に悪いバグを3つ捕捉している。難しいことではない。ただ読むだけだ。
Kent Beckの当初のTDDの枠組みは、100%のカバレッジや完璧な方法論についてのものではなかった。十分に速いフィードバックループを構築して、ミスが積み重なる前に捕捉することが本来の考え方だった。その考え方、すなわち速いフィードバックループは、2003年当時よりも今の方がはるかに関連性がある。なぜならAIは、これまで雇ったどの開発者よりも速くミスを犯すからだ。
---
FAQ
納期スピードは遅くなるか?
複雑なプロジェクトでは約10~15%程度。シンプルなプロジェクトではまったくないかもしれない。AIはテストを非常に素早く生成するため、オーバーヘッドは最小限になる。バグが発見されたときにリアルマネーの修正コストがかかるプロジェクト(ほとんどの実際のマネープロジェクトがこれに該当する)では、その15%は百倍の価値がある。
TypeScriptはどうか。強い型付けによって、多くのテストが不要になるのではないか。
部分的にはそうだ。TypeScriptはコンパイル時に、以前はテストが必要だった一連のエラーをキャッチする。しかし型はビジネスロジックをテストしない。割引計算関数が卸売顧客に対して正しいルールを適用しているかどうかを検証しない。それはあなたの責任だ。
テストを書いていないジュニアデベロッパーがAIコーディングツールを使うべきか。
いいえ。強い意見だ。テストなしでCopilotを使うジュニアデベロッパーは、本質的にオートパイロットで飛行機を操縦していることと同じだ。オートパイロットの仕組みやマニュアルで着陸する方法を理解せずに。AIは上級レベルに見えるコードを生成し、ジュニアはどの部分を信用できないかわからず、結局本番環境でインシデントが発生する。テストは少なくとも、受け入れているアウトプットを検証するためのメカニズムを与えてくれる。
そもそも、なぜテストをやめたのか。正直に答えてほしい。
部分的なバーンアウト。それから、すべてのプロジェクトが本当にシンプルで、テストが本当に価値を生み出していなかった時期があった。ミスは「常にテストすべき」とか「テストするな」とかではなく、特定のプロジェクトがどのカテゴリに該当するのかを理解し、それに応じて調整することに気づかなかったことだ。これが本当の教訓だ。
---
テスト記述は以前、保護のように感じられなかった。書類作業のように感じられた。AIがそれを変えた。AIが悪いからではなく、AIはむしろ私を意味のあるスピードアップさせたが、問題は新しい種類の自信に満ちた、よく整形された、もっともらしく見えるミスを導入したことだ。これらのミスは、かつてコードを読んで捕捉していた方法では捕捉できない。テストはAIのためではない。私のためだ。モデルが何を渡してくるにせよ受け入れる前に、コードが実際に何をする必要があるのかについて考えさせるための強制的な関数だ。
2年前にそのように考えていればよかったと思う。
関連記事: 今もプロダクトをリリースし続けるファウンダーとして毎日Claude Codeを活用する方法、ツール、およびSEO。
