「AIがコードを書く時代なら、テストはもう要らないのでは」という話を聞くたびに、私は少し違和感を持っていた。実際に手を動かしてみると、テストが要らなくなるのではなく、テストに求められる役割そのものが変わるという方が実感に近い。
この違和感を考える手がかりになったのが、ABAB↑↓BA氏の「AI開発時代だからこそ、テストの役割を見つめ直す」1という記事だった。ただし元記事の結論は「AIの時代になっても、テストの種類ごとの役割は変わらない」というもので、私が感じていた「役割そのものが変わる」という実感とは逆方向を指している。元記事が論じているのは、「E2Eテストが大事かユニットテストが大事か」という二択ではなく、各テスト手法は検出できる不具合の種類が異なるので、AI駆動開発ではその役割分担をこれまで以上に明確にすべきだという点だ。型検査・lintは静的な制約や問題を、ユニットテストは個別ロジックの振る舞いを検証し、統合テスト・E2Eテストは結合や実環境の検証を担い、既存の自動テストで想定されていない不具合を人間の探索的テストが補完する——という切り分けであり、この役割の定義自体は変わらないという立場である。
私はLaravel・Railsの個人開発・小規模受託案件で、PHPUnitのテスト自動生成、Feature Test中心のリリース前検証、GitHub ActionsでのCI/CD構築を実際に運用してきた。この記事では、元記事が投げた問題提起を自分の運用実績に当てて、「テストの役割が実際どう変わったか」を検証してみる。
前提
- 対象はLaravel(PHPUnit)の個人開発・小規模受託案件
- テストはFeature Test中心(ユニットテストは最小限)
- CI/CDはGitHub Actionsを使用
- レビュー担当は基本的に私1人(第三者レビューの代替として自動テストに依存する度合いが高い環境)
体験1: テスト一括生成で変わったのは「誰が正常系を書くか」
以前、テストが一本も無いLaravelプロジェクトに関わったことがある。コントローラーとモデルは育っているのに、品質を担保する手段が「手動でブラウザを操作して確認する」しかない状態だった。
このときClaude Codeに既存のコントローラーとモデルを読み込ませ、CLAUDE.mdにテストの命名規約とアサーション方針を明記した上でPHPUnitテストを一括生成させた。結果として、生成されたテストの大半はそのまま使えたが、ビジネスロジックの境界値(在庫がちょうどゼロになる瞬間や、割引率が0%・100%になる端の条件など)のテストは、私が手で追加する必要があった。
ここで起きた役割の変化は「テストを書く作業が減った」ことではない。正常系のカバレッジを埋める作業はAIに渡り、人間の役割は「AIが拾いにくい境界値をどこに設定するか」を判断することに絞られた、という変化だ。これは元記事の「ユニットテストはロジック検証を担う」という切り分けと同じ方向を指していると私は見ている。生成されたテストの型・アサーションの形が揃っていれば、個々のテスト内容を逐一読む個別レビューの負担は減らせる。ただし、それだけでは期待値が仕様と一致しているかどうかや、実装を意図的に壊したときにテストが失敗するかどうかまでは保証されないため、「形が揃っていること」と「境界値が足りているか」の確認を別物として扱う必要がある。
体験2: Feature Testが担うようになった「リリース判断の高速フィードバック」
別の案件では、PHP 7.4で稼働していた業務システムをPHP 8.1へ移行する必要があったが、依存パッケージの互換性が不明なまま一括で上げるのは怖かった。そこで既存のFeature Testを安全網にして、パッケージを1つ更新するたびにテストスイートを実行し、通った段階だけ次の更新に進める運用に変えた。結果、3週間という期間で、業務を止めずに8.1移行を完了できた。
これは単に「手動テストを自動化した」という話ではない。Feature TestはHTTPリクエストをLaravelアプリケーション内部で処理し、ルーティングやミドルウェア、対象実装、DB、レスポンスの結合を検証できるので、個々のユニットテストでは見えない「結合部分の不具合」を拾う。私の運用では、このレイヤーが「AIが生成・修正したコードを安全に積み重ねてよいかどうかの、リリース直前のゲート」という新しい役割を持つようになった。
以下は実際の案件からProductモデル・/api/ordersルート・ハンドラ・マイグレーションなど周辺実装を省いた非実行の概念例であり、この断片単体では実行できない。
<?php
namespace Tests\Feature;
use App\Models\Product;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class OrderCheckoutTest extends TestCase
{
use RefreshDatabase;
public function test_checkout_reduces_stock_and_creates_order(): void
{
$product = Product::factory()->create(['stock' => 10]);
$response = $this->postJson('/api/orders', [
'product_id' => $product->id,
'quantity' => 2,
]);
$response->assertStatus(201);
$this->assertDatabaseHas('orders', ['product_id' => $product->id]);
$this->assertEquals(8, $product->refresh()->stock);
}
}
このコード例は実際の案件を単純化したもので、今回の執筆にあたっては構文のみ確認した(実行はしていない=syntax-checked)。周辺実装を含まないため、この断片だけでは201応答やDB更新の実行可能性を検証できない非実行の概念例である。注文・在庫・レスポンスという3つの異なる層の結果を1つのテストで検証している点が、Feature Testの役割をそのまま表している。
体験3: CI/CDで「人間の確認」から「機械的なゲート」に変わったテスト
もう一つ、テスト実行とデプロイを手動で行っていた案件では、テスト実行忘れやデプロイ手順のミスが四半期に1回程度発生していた。GitHub Actionsでパイプラインを組み、mainブランチへのpush・vタグのpush・pull_request時に自動テスト、mainブランチへのpush時にステージングデプロイ、vタグをリモートへpushした時に本番デプロイが走る仕組みに変えたところ、テスト実行忘れとデプロイミスはゼロになり、デプロイ頻度も月2回から週2回に増えた。
以下は実際のワークフローからデプロイ処理部分を伏せ、トリガー条件とジョブ構成のみを再現した非実行の概念例である(deploy-staging・deploy-productionのステップは実際にはデプロイAPI呼び出しや認証処理が入るが、ここではプレースホルダーのechoに置き換えている)。
name: ci
on:
push:
branches: [main]
tags: ['v*']
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- run: composer install --no-interaction --prefer-dist
- run: cp .env.example .env && php artisan key:generate
- run: php artisan test --testsuite=Feature
env:
DB_CONNECTION: sqlite
DB_DATABASE: ':memory:'
deploy-staging:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- run: echo "deploy to staging"
deploy-production:
needs: test
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- run: echo "deploy to production"
このワークフローも同様に、今回は構文確認のみ(syntax-checked)で、実行結果の断定はしていない。デプロイ部分がプレースホルダーのため、このコードそのものから「デプロイが走る仕組み」を再現することはできない点に注意してほしい。ここで私が感じている役割の変化は、「テストが通ったことを人間が見て次の工程に進める」のではなく、「テストが通らなければ次の工程に進めない」という、人間の確認作業の代行から機械的なゲートへの転換だ。デプロイ頻度が増えたのは、人間の確認を減らしたからではなく、確認の抜け漏れをテストとパイプラインに押し付けられたから、というのが私の理解である。
比較表:私が運用の中で感じたテストの役割変化
| テストの階層 | AI駆動開発が本格化する前の役割 | 私が今感じている役割 |
|---|---|---|
| ユニット・型レベル | 正常に動くことの確認 | AIが生成したコードの精度を測る基準点 |
| Feature Test(統合層) | リリース前の総点検 | AIの変更を安全に積み重ねるための高速フィードバック |
| CI/CDパイプライン | 人間の作業手順の自動化 | 人間の確認を介さずに前へ進めてよいかの機械的なゲート |
| 境界値・探索的な確認 | 気づいた人が直す | AIが拾いにくいと分かった上で人間に割り当てる役割 |
元記事が「各テスト手法は検出できる不具合の種類で区別すべき」と述べている論旨と、この表の右列は同じ方向を向いているように私には見える。一方で、元記事が重視する「型設計への投資でテスト件数そのものを減らす」という論点は、私の案件ではまだ型を厳密にする投資まで手が回っておらず、裏取りできる実績を持っていない。この点は元記事の主張を鵜呑みにせず、今後自分の環境で試してから検証したい論点として残している。
AI駆動開発でテストの役割を見直すときに確認している4点
- そのテストは「AIの出力を検証する基準」になっているか、それとも「人間の作業の代行」のままか
- 失敗したときに戻ってくる場所(型・ユニット・Feature・E2Eのどのレイヤーで止めるべきか)が決まっているか
- 実行速度はAIの反復作業に耐えられるか(私の運用上の目安では数秒〜数十秒で結果が返ってくるか)
- 境界値や業務ルールの例外など、人間のドメイン判断が不可欠な領域を誰が・いつ見るかが決まっているか
まとめ
「AI時代にテストは要らなくなるか」という問いに対して、私の実運用から答えるなら「要らなくなるのではなく、人間が書く理由のあるテストだけが残る」という方が近い。正常系のカバレッジはAIに渡し、境界値の判断とパイプライン設計に人間の時間を使う——この線引きを意識してから、依存パッケージのアップグレードも段階的なテスト実行で安全に進められるようになり、デプロイ頻度が月2回から週2回に変わった。結局のところ、テストの本数や自動化の範囲より、「このテストは何を検出するためにあるのか」を自分で説明できることの方が、AI駆動開発では効いてくるというのが今回の実感である。
私はこのやり方に落ち着きましたが、もっと良い線引きがあれば知りたいところです。AI駆動開発でテストの役割をどう分担しているか——みなさんはどうしていますか? コメントで教えてもらえると助かります。
-
AI開発時代だからこそ、テストの役割を見つめ直す(ABAB↑↓BA・2026-09-29) ↩