Claude Media
Claude CodeでWordPressのPHPUnitを回す — wp-envとStop hookで失敗を戻す

Claude CodeでWordPressのPHPUnitを回す — wp-envとStop hookで失敗を戻す

WordPressプラグインのWP_UnitTestCaseをClaude Codeに書かせ、wp-envでPHPUnitを実行します。Stop hookでテスト失敗をClaudeに戻す設定までの手順です。

WordPressプラグインのテストは、普通のPHPUnitとは前提が違います。WP_UnitTestCaseを継承したテストは、WordPress本体を読み込んだデータベース付きの環境で動くからです。この環境を手元で用意する手間が、Claude Codeにテストを書かせる最初の壁になります。

wp-envを使えば壁は低くなります。Dockerの中にWordPressとテスト用ファイルが揃うので、phpunitをコマンド1つで呼べます。さらにStop hookを足すと、Claudeが作業を終えようとした瞬間にPHPUnitが走り、失敗があれば続きの作業として本人に戻ります。

WP_UnitTestCaseのテストとは何か

WP_UnitTestCaseは、WordPressのテストスイートが提供するテストの基底クラスです。公式ハンドブックによると、コアのテストクラスはすべてこれを継承します。基底クラスがテストごとにデータベースを既定の状態へ戻し、投稿タイプやタクソノミー、登録済みのフックなどを片付けます。

つまり、プラグインのテストも同じクラスを継承すれば、wp_insert_post()のような関数を本物のWordPressに対して呼べます。モックを組む必要が減る代わりに、実行にはWordPressとデータベースが要ります。

全体の流れは次のとおりです。

手順

PHPUnitを回すまでの流れ

  1. 1

    wp-envを起動する

    プラグインのフォルダでwp-env startを実行します。プラグインが有効化されたWordPressが立ち上がります。

  2. 2

    tests/bootstrap.phpを用意する

    テスト実行時にプラグインを読み込ませるファイルです。phpunit.xml.distと対で置きます。

  3. 3

    Claude Codeにテストを書かせる

    WP_UnitTestCaseのルールをCLAUDE.mdに書いておき、機能とテストを同時に作らせます。

  4. 4

    Stop hookで結果を戻す

    Claudeが止まる直前にPHPUnitを実行し、失敗の出力を本人に読ませます。

wp-envでPHPUnitを実行する

wp-envの公式READMEによると、環境にはWordPress本体に対応するPHPUnitのテストファイルが最初から入っています。その場所は環境変数WP_TESTS_DIRが指します。自分でテストライブラリを取得して配置する作業は要りません。

Composer、PHPUnit、WP-CLIもcliコンテナで使えます。実行はwp-env run <container> <tool>の形です。既定の作業ディレクトリはWordPressのルートなので、プラグインのテストを走らせるには--env-cwdでプラグインのフォルダを指定します。

wp-env start
wp-env run cli --env-cwd=wp-content/plugins/my-plugin composer install
wp-env run cli --env-cwd=wp-content/plugins/my-plugin phpunit

my-pluginは例です。プラグインのフォルダ名に置き換えます。--env-cwdの値は、先頭にスラッシュがなければWordPressのルートからの相対パスとして扱われます。

phpunitに引数を渡したいときは、wp-envが解釈する前に渡す必要があります。READMEは、競合するオプションをダッシュ2つの後ろに置く書き方を案内しています。

毎回長いコマンドを打たせない

Claude Codeに毎回この長い行を組み立てさせると、書き間違いが出ます。READMEは、package.jsonのscriptsにまとめる方法を例に挙げています。

{
  "scripts": {
    "test:php": "wp-env run cli --env-cwd=wp-content/plugins/my-plugin phpunit"
  }
}

こうしておけば、Claudeへの指示はnpm run test:phpだけで済みます。後で出すhookのスクリプトも同じコマンドを使い回せます。

tests/bootstrap.phpを用意する

プラグインのテストには、テスト実行時にそのプラグインを有効にするブートストラップが要ります。WP-CLIのwp scaffold plugin-testsは、そのための一式を生成するコマンドです。生成物は次のとおりです。

  • phpunit.xml.dist: PHPUnitの設定ファイル
  • tests/bootstrap.php: 現在のプラグインをテスト実行中に有効にするファイル
  • tests/test-sample.php: テストのサンプル
  • bin/install-wp-tests.sh: テストスイートとテスト用データベースを設定するスクリプト
  • CI用の設定ファイルと.phpcs.xml.dist

生成されたtests/bootstrap.phpはWP_TESTS_DIR環境変数を探します。wp-envの環境にはテストファイルとWP_TESTS_DIRが最初から入っているため、テストスイートを取得するbin/install-wp-tests.shの出番は、この構成では見込めません。phpunit.xml.distとtests/の側を使う形です。

ブートストラップを一から書かせるなら、次のような形になります。以下は構造を示す例で、公式のサンプルそのものではありません。

<?php
// tests/bootstrap.php(構造を示す例)
$tests_dir = getenv( 'WP_TESTS_DIR' );
 
if ( ! $tests_dir ) {
	echo "WP_TESTS_DIR が設定されていません。wp-env の中で実行してください。\n";
	exit( 1 );
}
 
require_once $tests_dir . '/includes/functions.php';
 
tests_add_filter( 'muplugins_loaded', function () {
	require dirname( __DIR__ ) . '/my-plugin.php';
} );
 
require $tests_dir . '/includes/bootstrap.php';

wp-tests-config.phpを自前のものに差し替えたい場合、READMEはWP_TESTS_CONFIG_FILE_PATH定数をbootstrap.phpで定義する方法を案内しています。環境が用意する既定の設定で足りるなら、触りません。

WP_UnitTestCaseのテストをClaude Codeに書かせる

テストの書き方には、WordPress流の決まりがあります。PHPUnit一般の知識だけで書かせると、動くけれど作法に外れたテストになります。決まりはCLAUDE.mdに固定しておくと、毎回の指示を省けます。

# PHPUnitテストの約束
 
## コマンド
- テスト実行: `npm run test:php`
- 1クラスだけ: `npm run test:php -- --filter クラス名`
 
## WP_UnitTestCaseのルール
- テストクラスは `WP_UnitTestCase` を継承する
- フィクスチャ用の関数は snake_case の `set_up()` / `tear_down()` を使う
- `set_up()` の最初の行で `parent::set_up()`、`tear_down()` の最後の行で `parent::tear_down()` を呼ぶ
- テスト用のユーザーや投稿は `self::factory()` で作る
- テストメソッド名は `test_` で始める
- 複数のアサーションがあるときは、失敗時のメッセージ引数を付ける

これらの決まりは、WordPressコアのハンドブックに書かれた内容に沿っています。camelCaseのsetUp()ではなくsnake_caseを使う理由は、ハンドブックによればPHPUnitのバージョン間の互換性を保つためです。

書かせるテストの形

プラグインに「ユーザーが公開した投稿の数を返す関数」があるとします。名前はmy_plugin_published_count()で、関数もこの記事の例です。テストは次のようになります。

<?php
// tests/test-published-count.php(構造を示す例)
class Tests_Published_Count extends WP_UnitTestCase {
 
	public function test_should_count_only_published_posts() {
		$user_id = self::factory()->user->create( array( 'role' => 'author' ) );
 
		self::factory()->post->create_many( 2, array(
			'post_author' => $user_id,
			'post_status' => 'publish',
		) );
		self::factory()->post->create( array(
			'post_author' => $user_id,
			'post_status' => 'draft',
		) );
 
		$this->assertSame(
			2,
			my_plugin_published_count( $user_id ),
			'下書きは数に含めない'
		);
	}
}

ここで見てほしいのは、テストの中で必要なデータを自分で作っている点です。

フィクスチャの作らせ方で外しやすい点

ハンドブックには、テストメソッドは空のWordPress環境から始まると書かれています。前のテストが作った投稿やユーザーは残りません。Claude Codeが「どこかにある既存の投稿」を前提にテストを書くと、単独では通っても全体では落ちます。

フィクスチャの指示で効くのは、次の3点です。

状況使うもの注意
1件だけ作る使うものself::factory()->post->create()注意返るのはIDです。オブジェクトが要るならcreate_and_get()
同種を複数作る使うものcreate_many( 3, array(...) )注意連番でユニークな値が入ります。メールアドレスの重複などが避けられます
クラス内で共有する使うものwpSetUpBeforeClass( $factory )注意staticで宣言し、self::factory()は呼ばずに渡された$factoryを使います

3つ目の注意は見落としやすい点です。ハンドブックは、wpSetUpBeforeClass()の中でself::factory()を呼ぶとユニーク性を担保するイテレーターが壊れる、と説明しています。Claudeに共有フィクスチャを書かせるときは、CLAUDE.mdにこの1行を足しておくと安全です。

同じ入力を少しずつ変えたテストを何本も書かせたくなったら、データプロバイダーを使うよう指示します。ハンドブックは、コピーしたテストメソッドよりデータプロバイダーを勧めています。

Stop hookでphpunitの失敗をClaudeに戻す

テストを書かせても、Claudeが「通りました」と報告するだけでは信用できません。実際に走らせた結果を、本人に突き返す仕組みにします。

Stopは、メインのエージェントが応答を終えたときに動くイベントです。ユーザーが中断した場合は動きません。このhookが終了コード2で終わると、Claudeは標準エラー出力の内容を「続けるべき理由」として受け取ります。つまりPHPUnitの失敗出力を標準エラーに出して終了コード2で抜ければ、Claudeはそれを読んで直しに戻ります。

スクリプトを書く

.claude/hooks/run-phpunit.shを作ります。

#!/bin/bash
# Docker が動いていなければ何もしない
docker info >/dev/null 2>&1 || exit 0
 
cd "$CLAUDE_PROJECT_DIR" || exit 0
 
# PHP ファイルに変更がなければ実行しない
git status --porcelain | grep -q '\.php$' || exit 0
 
OUT=$(wp-env run cli --env-cwd=wp-content/plugins/my-plugin phpunit 2>&1)
STATUS=$?
 
if [ "$STATUS" -ne 0 ]; then
  echo "PHPUnit が失敗しました(終了コード $STATUS)。出力の末尾です。" >&2
  echo "$OUT" | tail -n 60 >&2
  exit 2
fi
exit 0

コマンド型のhookには、CLAUDE_PROJECT_DIRなどが環境変数として渡されます。実行権限を付けておきます。

chmod +x .claude/hooks/run-phpunit.sh

最初の2つのexit 0には理由があります。Dockerが止まっているとwp-envは失敗し、そのたびにClaudeへ「失敗」を戻してしまいます。Claudeにはどうにもできない原因なので、環境の問題は黙って通します。PHPファイルに変更がないターンも同様で、質問に答えただけの応答のたびに、数十秒かかるテストを走らせる理由がありません。

settings.jsonに登録する

.claude/settings.jsonに次を足します。

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/run-phpunit.sh",
            "timeout": 300
          }
        ]
      }
    ]
  }
}

timeoutの既定値は、command型のhookで600秒です。wp-envの初回起動が絡むとテストが長引くことがあるので、ここでは300秒を明示しました。テストが重いプラグインなら、実測に合わせて調整します。

止まらない場合の歯止め

失敗が直らないテストでhookが毎回ブロックすると、無限に続きそうに見えます。公式の仕様にはその歯止めがあります。

  • 入力JSONのstop_hook_activeは、すでにStop hookの結果で続行中のときにtrueになる
  • Stop hookが連続で8回続行させると、次のブロックは上書きされてターンが終わる
  • 連続回数は、Claudeがツールを呼ぶたびにリセットされる
  • 上限は環境変数CLAUDE_CODE_STOP_HOOK_BLOCK_CAPで変えられる

公式は、永遠に解決しない条件でブロックし続けないよう、stop_hook_activeか会話履歴を確認することを案内しています。PHPUnitのように「直せば通る」条件なら、このスクリプトのように毎回実行して構いません。環境の故障のような直せない原因は、先頭のexit 0で逃がしています。

動作を確かめる

hookが正しく働くか、わざと壊して確かめます。

  1. 関数の戻り値を書き換えるなど、テストが落ちる変更をClaudeにさせる
  2. 応答の終わりに、PHPUnitの失敗出力がClaudeへ戻ることを確かめる
  3. Claudeが直し、再度テストが通ることを確かめる

hookの設定が反映されていない場合は、/hooksメニューで登録内容を見ます。

特定のテストだけ実行したいときは、--filterやグループで絞れます。WordPressのハンドブックでは、クラス名を--filterに渡す方法と、@groupで分けて--groupを指定する方法が紹介されています。

npm run test:php -- --filter Tests_Published_Count
npm run test:php -- --group my-plugin

出力の記号は、.が成功、Fが失敗、EがPHPのエラーや警告による失敗、Sがスキップ、Rがリスキー、Iが未実装です。EはPHP側のエラー、Fはアサーションの不一致なので、出力を読むときに見分けられます。

よくあるつまずき

テストファイルが実行されない

メソッド名がtest_で始まらないと、PHPUnitは実行しません。ハンドブックにある仕様です。Claudeがit_counts_posts()のような名前で書いたら、ここで落とし穴にはまっています。CLAUDE.mdの約束に入れておく理由はこれです。

set_upを書くと他のテストが壊れる

set_up()の最初にparent::set_up()を呼び忘れると、基底クラスのデータベース初期化などが走りません。ハンドブックは、set_up()の先頭とtear_down()の末尾で親を呼ぶことを必須としています。

wp-envの環境が前回の状態を引きずる

wp-env cleanupで環境をリセットできます。消えるのはデータベースだけではなく、コンテナ、ボリューム、ローカルファイルも含みます。イメージまで消して作り直すならwp-env destroyです。どちらも、まずClaudeではなく人が実行を確認してから使う操作にしておくと安全です。

まとめ

WordPressのPHPUnitをClaude Codeに任せる構成は、3つの部品で成り立ちます。wp-envがWordPressとテストファイルを用意し、CLAUDE.mdがWP_UnitTestCaseの作法を固定し、Stop hookが実行結果を本人に戻します。

最初に整えるのは、npm run test:phpのような1コマンドです。これがあるだけで、CLAUDE.mdの指示もhookのスクリプトも短く書けます。

ブロック開発の側からwp-envを使う場合はClaude CodeでGutenbergブロックを作る、プラグイン自体の書き方はClaude CodeでWordPressプラグイン開発を始めるにまとめています。PHPUnitからPestへ移すならPHPUnitからPestへの移行手順が参考になります。Stop hookの再発火が気になる場合は/goal Stop hookが止まらない症状と回避策を見てください。

参照した一次ソース

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

  1. code.claude.com/docs/en/hooks
  2. make.wordpress.org/core/handbook/testing/automated-testing/phpunit
  3. make.wordpress.org/core/handbook/testing/automated-testing/writing-phpunit-te…
  4. github.com/WordPress/gutenberg/blob/trunk/packages/env/README.md
  5. developer.wordpress.org/cli/commands/scaffold/plugin-tests
この記事を共有:XはてブLinkedIn