Claude Media
Claude CodeでPHPUnitからPestへ移行する手順 — 通過数を照合しつつ変換

Claude CodeでPHPUnitからPestへ移行する手順 — 通過数を照合しつつ変換

PHPUnitのテストをPestへ移す作業をClaude Codeで進める手順です。pest-plugin-driftで機械変換し、手作業が要る部分をDatasetsへ寄せ、変換前後の通過数が一致することを完了条件にします。

PHPUnitからPestへの移行は、半分が機械的な変換で、半分が人の判断です。変換そのものはPest側のプラグインがほぼ片付けます。時間がかかるのは、変換後のテストが元と同じものを検証しているかを確かめる作業と、似たテストをまとめ直す作業のほうです。

Claude Codeは後者に向いています。変換の前に通過数を記録させ、1ファイルずつ変換して数を照合させれば、「Pestになったが、いつの間にかテストが減っていた」という事故を避けられます。この記事では、その順番と各段階の指示文、権限設定を説明します。

移行の流れ

手順

PHPUnitからPestへの移行手順

  1. 1

    PHPUnit側で通過数を記録する

    移行前に php artisan test を実行し、通過したテスト数を baseline.txt に残します。

  2. 2

    Pestとdriftプラグインを入れる

    PHPUnitを外してPestを入れ、pest-plugin-drift を開発依存に足します。

  3. 3

    フォルダ単位でdriftを実行する

    --drift にパスを渡し、小さなフォルダから変換します。

  4. 4

    手作業の部分をClaude Codeに直させる

    driftが変換しきれなかったテストと、重複しているテストをDatasetsに寄せます。

  5. 5

    通過数を照合して次へ進む

    変換前と同じ数が通過したことを確かめてから、次のフォルダへ移ります。

何が機械でできて、何が人の仕事か

Pestは内部でPHPUnitの上に作られているため、移行自体は短い手順で済みます。変換を担うのが pest-plugin-drift で、./vendor/bin/pest --drift を実行すると、PHPUnitのテストをPestの書き方に書き換えます。

ガイドに載っている変換例は次のとおりです。クラスとメソッドが関数になり、assertTrue が expect() の連鎖に変わります。

<?php
 
namespace Tests\Unit;
 
use PHPUnit\Framework\TestCase;
 
class ExampleTest extends TestCase
{
    public function test_that_true_is_true(): void
    {
        $this->assertTrue(true);
    }
}

これを変換すると、次の形になります。

test('true is true', function () {
    expect(true)->toBeTrue();
});

ガイドは、ほとんどのテストは自動変換で問題なく動くとしたうえで、一部のテストは手作業での変換が必要になることがあると書いています。どのテストが手作業になるかを列挙した記載はありません。つまり、変換後に「どれが漏れたか」を見つける工程は、自分たちで用意する必要があります。ここがClaude Codeの出番です。

役割分担を表にします。

作業担当理由
クラスから関数への書き換え担当driftプラグイン理由規則的な変換で、再現性がある
変換前後の通過数の照合担当テスト実行の結果理由数字で判定でき、解釈が要らない
変換漏れの発見と手直し担当Claude Code理由差分を読んで判断する作業
似たテストをDatasetsへ寄せる担当Claude Code理由意味の同じテストを見分ける作業

手順1: PHPUnitのまま通過数を記録する

最初にやるのは、何も変えずにテストを走らせて数を残すことです。この数が、移行の完了条件になります。

php artisan test 2>&1 | tee baseline.txt | tail -n 5

出力の最後に出る通過数を、Claude Codeにも読ませます。ブランチは移行専用に切り、baseline.txt はコミットしないか、作業が終わるまで手元に置きます。ベースラインが取れていない状態でdriftを実行すると、あとから「元は何本通っていたか」を確かめる手段がなくなります。

LaravelアプリのPHP環境の整え方は、Claude CodeとLaravelの開発フローで扱っています。ここでは、すでに php artisan test が通る状態から始めるものとします。

手順2: Pestとdriftを入れる

Pestのインストール手順は、PHPUnitを外してからPestを入れる順です。PestはPHP 8.4以上を必要とします。PHPのバージョンが足りない場合は、先にPHP側を上げる必要があります。バージョン更新の手順は、Rectorを使ったPHP 7から8への更新で扱っています。

composer remove phpunit/phpunit
composer require pestphp/pest --dev --with-all-dependencies
./vendor/bin/pest --init
composer require pestphp/pest-plugin-drift --dev

pest --init は、テストスイートのルートに設定ファイル Pest.php を作ります。この手順を、Claude Codeに任せるかどうかは判断が分かれます。composer.json と composer.lock が変わるため、依存の変更だけは自分で実行し、結果をコミットしてからClaude Codeに渡すと、あとの差分が読みやすくなります。

手順3: 権限とCLAUDE.mdを先に整える

変換を始める前に、Claude Codeが確認なしで走らせてよいコマンドを決めます。テスト実行は何度も繰り返すため、毎回許可を求められると作業が止まります。一方で、composer や git push は自動で通したくありません。

Claude Codeのallowルールは、* を含む文字列でコマンドにマッチさせます。.claude/settings.json に次のように書きます。

{
  "permissions": {
    "allow": [
      "Bash(php artisan test *)",
      "Bash(./vendor/bin/pest *)",
      "Bash(git diff *)"
    ],
    "deny": [
      "Bash(git push *)"
    ]
  }
}

* はサブコマンドの後ろに置きます。Bash(git *) のように手前を広げると、git push まで通ってしまうためです。git push を別の書き方で打たれた場合はdenyにマッチしません。この限界はpermissionsの説明にも書かれているので、mainなど重要なブランチの保護は、GitHubのブランチ保護などリポジトリ側の設定でも掛けておくと確実です。

次に、CLAUDE.mdへ移行のルールを書きます。変換の途中で会話が長くなっても、ルールが毎回読み直されます。

## PHPUnitからPestへの移行ルール
- 変換は1フォルダずつ。`./vendor/bin/pest --drift tests/Unit/Billing` のようにパスを渡す
- 変換の前後で `php artisan test` の通過数を比べる。baseline.txt が基準
- 通過数が減ったら、そのフォルダの変換を戻して原因を調べる
- テストを削除しない。重複に見えても、まず報告して指示を待つ
- アサーションを弱めない(例: toBe を toBeTruthy にしない)

最後の2行が大事です。通過数を合わせるだけなら、失敗するテストを消せば数字は揃うからです。テストの削除と、アサーションの緩和は、禁止として明文化します。

手順4: フォルダ単位でdriftを実行する

driftは、パスを最初の引数に渡すと、そのフォルダだけを変換します。ガイドの例では tests/Helpers を指定しています。

./vendor/bin/pest --drift tests/Unit/Billing
php artisan test

実行後に、変換したファイル数を含む概要が表示されます。そのまま php artisan test を走らせ、通過数を baseline.txt と見比べます。

小さなフォルダから始める理由は、失敗したときの原因の範囲を狭めるためです。全体を一度に変換すると、通過数が合わなかったときに、どのファイルが原因かを探す作業がまるごと発生します。

通過数が合わなかったときは、Claude Codeに次のように指示します。

tests/Unit/Billing の変換後に、通過数が baseline.txt より減っています。
git diff でこのフォルダの変換前後を比べ、減った原因のテストを特定してください。
特定できたら、元のアサーションと同じ強さを保ったままPestの書き方に直してください。
テストを削除してはいけません。

手順5: 似たテストをDatasetsへ寄せる

機械変換が終わると、クラス形式はなくなりますが、テストの重複は残ります。PHPUnitでは、入力だけが違う同じテストを、メソッドごとに書き並べていることがよくあります。

PestのDatasetsは、テストデータの配列を定義すると、Pestが同じテストをデータ1件ごとに自動で実行する機能です。ガイドの例では、with() にメールアドレスの配列を渡しています。

it('has emails', function (string $email) {
    expect($email)->not->toBeEmpty();
})->with(['enunomaduro@gmail.com', 'other@example.com']);

引数を複数にしたいときは、配列の配列を渡します。データに文字列のキーを付けると、実行結果の説明にそのキーが出ます。

it('has emails', function (string $email) {
    expect($email)->not->toBeEmpty();
})->with([
    'james' => 'james@laravel.com',
    'taylor' => 'taylor@laravel.com',
]);

重複の検出と書き換えは、Claude Codeに任せます。ただし、寄せる対象は「入力以外が完全に同じ」テストに限ります。

tests/Unit/Billing の中で、入力値だけが違い、アサーションの形が同じテストを探してください。
見つけたら、一覧にして報告してください。まだ書き換えないでください。

まず報告させ、候補を人が確認してから書き換えさせます。名前が似ていても、検証している振る舞いが違うテストはまとめてはいけません。確認が済んだ候補だけを、次の指示で書き換えます。

報告した候補のうち、1番と3番だけをDatasetsにまとめてください。
各データには、失敗時に分かる名前をキーとして付けてください。
書き換えたら php artisan test を実行し、通過数を baseline.txt と比べてください。

ここで通過数の読み方に注意が要ります。3つのテストを、3件のデータを持つ1つのテストにまとめた場合、実行結果の表示では1つのテストがデータ1件ごとに行として並びます。表示上のテスト数が元と合うかどうかは、まとめ方で変わります。合わなければ、減ったのか表示の数え方が変わったのかを、Pestの出力を見て分けてください。Datasetsで寄せた後は、基準を「通過数」から「元のテスト名が出力に残っていること」に切り替える判断もありえます。

手順6: 1ファイルずつ非対話で回す

フォルダの中のファイルが多いときは、ファイルごとにClaude Codeを非対話モードで呼ぶと、会話が肥大化せずに済みます。claude -p はプロンプトを渡すと結果を出力して終了し、--allowedTools で自動承認するツールを指定します。

for f in tests/Unit/Billing/*Test.php; do
  claude -p "$f がPestに変換済みか確認し、残っているPHPUnit形式を直す。\
アサーションを弱めない。テストを削除しない" \
    --allowedTools "Read,Edit,Bash(./vendor/bin/pest *)"
  php artisan test | tail -n 3
done

ループの中で毎回 php artisan test を実行し、通過数を目で追えるようにしています。数が下がったファイルで止めたければ、tail の出力を変数に取って比較する形に変えます。

通過数が合わないときの原因

数が合わない原因は、大きく3つに分かれます。

原因

通過数が減ったときの切り分け

  • テストが実行されなくなった

    関数形式にしたあと、ファイルがテストとして拾われていないことがあります。./vendor/bin/pest を単独で実行し、そのファイルのテスト名が出力に出るかを確認します。

  • Datasetsで行が変わった

    まとめたテストの数え方が変わっただけの場合です。元のテスト名が、データ名として出力に残っているかを見ます。

  • 手直しでアサーションが抜けた

    Claude Codeが書き換えた際に、検証が落ちていることがあります。git diff でassertの数を比べ、減っていれば戻させます。

Pestの実行結果の末尾には、通過したテスト数と、アサーション数が並びます。アサーション数は、assertTrue を expect() の連鎖に直すと、数え方が変わりうる値です。基準にするのは通過したテストの数で、アサーション数はあくまで補助として扱います。

他のフレームワーク移行との違い

同じ「テストを別のフレームワークへ移す」作業でも、JavaScriptのJestからVitestへの移行は、モックの書き換えが主な難所です。手順はClaude CodeでJestからVitestへ移行するにあります。

PHPUnitからPestの場合は、Pestが内部でPHPUnitの上に作られており、変換はプラグインが担います。人の作業は、機械変換の漏れの回収と、Datasetsによる整理が中心です。その代わり、PHPのバージョン要件が移行の前提になります。

まとめ

移行の完了条件は、Pestになったことではなく、元と同じテストが同じように通っていることです。ベースラインを先に取り、フォルダ単位で変換し、毎回数を照合する。この3つを守れば、Claude Codeに任せる範囲を広げても、結果を数字で検証できます。

Datasetsへの整理は、移行が終わってから別のコミットで行う選択肢もあります。変換とリファクタリングを同じ差分に混ぜないほうが、通過数が合わなかったときに原因を絞りやすくなります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. pestphp.com/docs/migrating-from-phpunit-guide
  2. pestphp.com/docs/installation
  3. pestphp.com/docs/datasets
  4. code.claude.com/docs/en/common-workflows
  5. code.claude.com/docs/en/permissions
  6. code.claude.com/docs/en/headless
この記事を共有:XはてブLinkedIn