2026年9月12日

2026年9月12日

WordPressのPHPUnitモックをwp-envで活用するユニットテスト実践

wp-env を使うと、Docker 上にテスト専用の WordPress とデータベースを立ち上げ、その中で PHPUnit を実行できます。外部APIやメール送信のように「テストのたびに本物を動かしたくない処理」は、WordPress のフィルター(pre_http_request など)とテスト用ライブラリの仕組み、PHPUnit のモックを組み合わせて差し替えます。この記事では、wp-env でのテスト環境づくりから、HTTP通信・メール・自作クラスをそれぞれモックするテストの書き方、つまずきやすい点までを順に説明します。

なお、WordPress を読み込まずに関数単体をテストする方法(Brain Monkey などで WordPress 関数そのものを差し替える方式)もありますが、この記事は「WordPress を実際に動かしたうえで、外部との境界だけをモックする」統合寄りのテストを扱います。

先に全体像を押さえる

登場するものは次の4つです。

  • wp-env:@wordpress/env パッケージのコマンド。Docker で開発用とテスト用の WordPress 環境を起動します。
  • WordPress のテストライブラリ:WP_UnitTestCase やテスト用データを作るファクトリーなど。wp-env のコンテナには、インストールされている WordPress に対応するテストファイルが含まれており、場所は環境変数 WP_TESTS_DIR で参照できます。
  • PHPUnit と Yoast PHPUnit Polyfills:Composer でプラグインに入れます。WordPress のテストライブラリは Polyfills を必要とします。
  • モックの手段:WordPress のフィルター、テストライブラリのメール用モック、PHPUnit の createMock()。

WordPress のテストライブラリが対応する PHPUnit のバージョンには上限があり、最新の PHPUnit がそのまま使えるとは限りません(2026年10月現在)。導入前に WordPress の開発者向けハンドブックで対応バージョンを確認し、Composer でバージョンを固定してください。

手順1:wp-env を準備する

Docker(Docker Desktop など)と Node.js が必要です。プラグインのルートで wp-env を入れ、設定ファイルを置きます。

npm install --save-dev @wordpress/env
{
	"core": null,
	"plugins": [ "." ]
}

上の内容を .wp-env.json として保存します。"plugins": [ "." ] は「このディレクトリをプラグインとして読み込む」という意味です。"core": null は最新の WordPress を使う指定で、特定のバージョンで試したい場合はここを変えます。

npx wp-env start

起動すると開発用(通常はポート 8888)とテスト用(通常は 8889)の2つの環境ができます。テストはテスト用環境で動かすので、開発用環境で作った投稿や設定がテスト結果に混ざることはありません。

手順2:PHPUnit とブートストラップを用意する

Composer で PHPUnit と Polyfills を開発用依存として追加します。PHPUnit のバージョンは手順の冒頭で確認した対応範囲に合わせてください。以下は 9 系に固定する例です。

composer require --dev phpunit/phpunit:^9 yoast/phpunit-polyfills

vendor/ はプラグインのディレクトリ内にできるため、コンテナにもそのままマウントされます。

次に tests/bootstrap.php を作ります。WP-CLI の wp scaffold plugin-tests が生成するものとほぼ同じ構成です。

<?php
// tests/bootstrap.php
$_tests_dir = getenv( 'WP_TESTS_DIR' );

if ( ! $_tests_dir ) {
	$_tests_dir = rtrim( sys_get_temp_dir(), '/\\' ) . '/wordpress-tests-lib';
}

// Polyfills の場所をテストライブラリに伝える
define( 'WP_TESTS_PHPUNIT_POLYFILLS_PATH', dirname( __DIR__ ) . '/vendor/yoast/phpunit-polyfills' );

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';

phpunit.xml.dist はプラグインのルートに置きます。

<?xml version="1.0"?>
<phpunit bootstrap="tests/bootstrap.php" colors="true">
	<testsuites>
		<testsuite name="plugin">
			<directory prefix="test-" suffix=".php">./tests/</directory>
		</testsuite>
	</testsuites>
</phpunit>

テストはテスト用環境の CLI コンテナで実行します。--env-cwd でプラグインのディレクトリに移動してから PHPUnit を呼びます。

npx wp-env run tests-cli --env-cwd=wp-content/plugins/my-plugin vendor/bin/phpunit

my-plugin の部分はプラグインのディレクトリ名に置き換えます。wp-env のバージョンによってコマンドの書き方が変わってきた経緯があるため、エラーになる場合は npx wp-env run --help で使えるオプションを確認してください。毎回打つのは手間なので、package.json の scripts に "test:php" として登録しておくと便利です。

テスト対象のコード例

ここからは、次のような小さなクラスを例にします。外部の天気APIを呼び、結果をトランジェントにキャッシュし、取得に失敗したら管理者にメールで知らせます。ログ出力は Logger インターフェース経由にしてあり、テストでは差し替えられるようにしています。

<?php
// src/class-weather-client.php
interface My_Plugin_Logger {
	public function error( string $message ): void;
}

class My_Plugin_Weather_Client {
	private $logger;

	public function __construct( My_Plugin_Logger $logger ) {
		$this->logger = $logger;
	}

	public function get_temperature( string $city ) {
		$cache_key = 'my_plugin_temp_' . md5( $city );
		$cached    = get_transient( $cache_key );
		if ( false !== $cached ) {
			return (float) $cached; // DB経由だと文字列で戻るため型をそろえる
		}

		$response = wp_remote_get(
			add_query_arg( 'city', rawurlencode( $city ), 'https://api.example.com/v1/weather' ),
			array( 'timeout' => 5 )
		);

		if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
			$this->logger->error( 'weather api failed: ' . $city );
			wp_mail( get_option( 'admin_email' ), '天気APIの取得に失敗しました', '都市: ' . $city );
			return null;
		}

		$data = json_decode( wp_remote_retrieve_body( $response ), true );
		if ( ! is_array( $data ) || ! isset( $data['temp'] ) ) {
			return null;
		}

		$temp = (float) $data['temp'];
		set_transient( $cache_key, $temp, 10 * MINUTE_IN_SECONDS );
		return $temp;
	}
}

このコードには、テストで本物を動かしたくない箇所が3つあります。HTTP通信、メール送信、ロガーです。

モック1:HTTP通信を pre_http_request で差し替える

WordPress の HTTP API(wp_remote_get() など)は、実際に通信する前に pre_http_request フィルターを通します。このフィルターが false 以外の値を返すと、通信は行われずその値がレスポンスとして使われます。引数は「レスポンス(既定は false)」「リクエストの引数」「URL」の3つです。

<?php
// tests/test-weather-client.php
class Test_Weather_Client extends WP_UnitTestCase {

	private $requested_url = '';

	public function set_up(): void {
		parent::set_up();
		add_filter( 'pre_http_request', array( $this, 'fake_http' ), 10, 3 );
		reset_phpmailer_instance();
	}

	public function tear_down(): void {
		remove_filter( 'pre_http_request', array( $this, 'fake_http' ), 10 );
		parent::tear_down();
	}

	public function fake_http( $preempt, $args, $url ) {
		$this->requested_url = $url;
		return array(
			'headers'  => array(),
			'body'     => wp_json_encode( array( 'temp' => 21.5 ) ),
			'response' => array( 'code' => 200, 'message' => 'OK' ),
			'cookies'  => array(),
			'filename' => null,
		);
	}

	public function test_returns_temperature_from_api() {
		$logger = $this->createMock( My_Plugin_Logger::class );
		$client = new My_Plugin_Weather_Client( $logger );

		$this->assertSame( 21.5, $client->get_temperature( 'Tokyo' ) );
		$this->assertStringContainsString( 'city=Tokyo', $this->requested_url );
	}
}

set_up() / tear_down() が setUp() ではなくスネークケースになっているのは、Polyfills を介して PHPUnit の複数バージョンに対応するための WordPress テストライブラリの約束です。

フィルターの解除を忘れると、同じプロセスで動く別のテストにも偽のレスポンスが返り、原因の分かりにくい失敗につながります。WP_UnitTestCase はテストごとに追加されたフックを元に戻す処理を持っていますが、自分で追加したものは自分で外しておくほうが読み手にも意図が伝わります。

モック2:メール送信をテスト用メーラーで確かめる

WordPress のテストライブラリは、本物の送信を行わないモックのメーラーを wp_mail() に組み込んでいます。送信されたはずの内容は tests_retrieve_phpmailer_instance() で取り出せます。前のテストの送信内容が残らないよう、set_up() で reset_phpmailer_instance() を呼んでいます。

失敗時のテストでは、pre_http_request から WP_Error を返して通信エラーを再現します。

	public function test_sends_mail_and_logs_when_api_fails() {
		remove_filter( 'pre_http_request', array( $this, 'fake_http' ), 10 );
		add_filter( 'pre_http_request', function () {
			return new WP_Error( 'http_request_failed', 'timeout' );
		} );

		$logger = $this->createMock( My_Plugin_Logger::class );
		$logger->expects( $this->once() )
			->method( 'error' )
			->with( $this->stringContains( 'Osaka' ) );

		$client = new My_Plugin_Weather_Client( $logger );
		$this->assertNull( $client->get_temperature( 'Osaka' ) );

		$sent = tests_retrieve_phpmailer_instance()->get_sent();
		$this->assertNotFalse( $sent );
		$this->assertSame( '天気APIの取得に失敗しました', $sent->subject );
	}

このテストは上のクラスに追加するメソッドです。get_sent() は送信された順に内容を返し、宛先・件名・本文などを確かめられます。モックのメーラーの細かな仕様はテストライブラリのバージョンで変わることがあるため、手元の WP_TESTS_DIR 配下の includes/mock-mailer.php を開いて確認しておくと安心です。

別の方法として、pre_wp_mail フィルター(WordPress 5.7 で追加)で送信そのものを止め、渡された引数を記録するやり方もあります。テストライブラリに依存したくない場合はこちらが使えます。

モック3:自作クラスを PHPUnit のモックで差し替える

ロガーのように自分で定義した依存は、PHPUnit の createMock() で差し替えます。上の失敗時テストでは、expects( $this->once() ) で「error() が1回だけ呼ばれること」、with() で「引数に都市名が含まれること」を検証しています。呼ばれなかった場合や2回呼ばれた場合はテストが失敗します。

この差し替えができるのは、クラスの中で new せずにコンストラクターで受け取る形(依存性の注入)にしているからです。テストしにくいと感じるコードは、まず依存をコンストラクターや引数で受け取るように変えると、モックを当てる場所が生まれます。逆に、WordPress のコア関数(get_option() など)は実物が動いているので、モックせずにテスト用データを入れて確かめるのが wp-env での統合テストの考え方です。オプションなら update_option()、投稿なら self::factory()->post->create() で用意します。

トランジェントのキャッシュを確かめる

キャッシュが効いていることも確認しておきます。2回呼んで、HTTP が1回しか呼ばれないことを数えます。

	public function test_uses_cache_on_second_call() {
		$count = 0;
		add_filter( 'pre_http_request', function ( $preempt ) use ( &$count ) {
			$count++;
			return $preempt;
		}, 5 );

		$client = new My_Plugin_Weather_Client( $this->createMock( My_Plugin_Logger::class ) );
		$client->get_temperature( 'Nagoya' );
		$client->get_temperature( 'Nagoya' );

		$this->assertSame( 1, $count );
	}

優先度 5 で先に数えてから $preempt(この時点では false)を返し、優先度 10 の fake_http() が偽のレスポンスを返す、という順番になっています。テスト用環境のデータベースはテストごとに巻き戻されるため、ほかのテストでセットしたトランジェントが残ることは通常ありません。ただし永続オブジェクトキャッシュを使うドロップインを入れている場合は挙動が変わるので、テスト環境には入れないようにします。

つまずきやすい点

  • **WP_TESTS_DIR が空になる**:テストを開発用環境(cli)で実行しているかもしれません。tests-cli を指定しているか確認してください。
  • Polyfills が見つからないというエラー:composer install を実行していないか、WP_TESTS_PHPUNIT_POLYFILLS_PATH のパスがずれています。
  • PHPUnit のバージョンが合わない:テストライブラリが対応していない新しいメジャーバージョンを入れるとブートストラップで止まります。対応範囲に合わせて固定し直します。
  • プラグインが読み込まれていない:muplugins_loaded で読み込むファイルのパスを確認します。メインファイルの名前を変えた場合も見落としがちです。
  • ローカルの PHP とコンテナの PHP の違い:Composer はホスト側で実行することが多いため、依存パッケージがコンテナの PHP バージョンに対応しているかも確認しておきます。wp-env では .wp-env.json の phpVersion で PHP のバージョンを指定できます。

まとめ

  • wp-env はテスト用の WordPress とデータベースを別に用意し、npx wp-env run tests-cli でその中から PHPUnit を実行できます。
  • テストライブラリの場所は WP_TESTS_DIR、Polyfills の場所は WP_TESTS_PHPUNIT_POLYFILLS_PATH でつなぎます。PHPUnit の対応バージョンは事前に確認して固定します。
  • HTTP通信は pre_http_request、メールはテスト用メーラー(または pre_wp_mail)、自作の依存は createMock() で差し替えます。
  • WordPress のコア関数は実物を使い、外部との境界だけをモックするのが統合テストでの線引きです。
  • 追加したフィルターはテストごとに外し、テスト同士が影響し合わないようにします。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲