WordPress&PHPUnitのモダンユニットテスト環境をwp-envを使って構築する

記事内に広告が含まれていることがあります。

WordPressはPHPUnit関連に限らず良くも悪くも歴史あるプロジェクトなのでレガシーな情報にぶち当たることが非常に多いです。

特にユニットテストに関しては正直関心も低いのか情報が多いとは言えません。

まして公式の情報も充実しているとは言いづらく非常に分かりづらいので、執筆時時点で最もモダンなwp-envを使ったPHPUnitの実行環境整備を記します。

WordPressカテゴリーCTA
\ココナラでWordPressエラー修正サービス始めました!/

ココナラでWordPressのエラー解決サービスを始めました!

追加オプション無しで3,000円から受け付けておりますが、新規会員登録で300円、僕の紹介コードを入力すると1,000円分のポイントが貰えるので実質1,700円から利用することができます!

  • 突然画面が真っ白になった
  • 英語のエラーが表示されてどうしたらいいかわからない
  • とにかく困っている

どんなことでもぜひお気軽にご相談ください!

紹介コード:K0GR23
スポンサーリンク
スポンサーリンク

WordPress&PHPUnit&wp-envの要点

この章でコードと要点だけを網羅し、後半で各種解説を行います。

GitHubリポジトリ

当記事で解説している環境のリポジトリは以下をご覧ください。

git cloneしてreadme.mdに記載の通りにコマンドを叩けば実際に稼働させることができます。

GitHub Repository: braveryk7/WordPress-PHPUnit-use-wp-env-sample

要点

  • npmの@wordpress/envパッケージを使ってwp-env環境を使用する
  • composerのPHPUnitとyoast/PHPUnit-Polyfillsとyoast/wp-test-utilsパッケージを使用する
  • そのためnpmとcomposer両方の環境整備が必須
  • wp-envは内部でdockerを使用しているためdocker(docker-compose)が必須

以下は「WordPress PHPUnit」等で検索した時に得られる情報で誤ってるもの、現在は必要ないものの要点です。

  • composerのwp-cliパッケージはPHPUnit実行に関しては必要無い
  • wp scaffold plugin-testsコマンドも必要ない
  • 上記のコマンドで出力されるbin/install-wp-tests.shも必要ない
  • wp-env環境でDockerを使用するが、docker-compose.ymlを用意する必要はない(wp-envが動的生成する)

これらの情報が決して間違っている訳ではないですが、既にレガシーとなってしまった情報やもっと簡単に実行できる方法があります。

またこれらの情報も使い方によってはまだまだ有効な方法です。

環境

僕の環境構築時点での各種バージョンです。

$ node -v
v18.15.0

$ npm -v
9.5.0

$ composer -v
Composer version 2.5.3 2023-02-10 13:23:52

$ docker -v
Docker version 20.10.22, build 3a2c30b

$ docker-compose -v
Docker Compose version v2.15.1
# composer.json
{
    "require-dev": {
        "yoast/phpunit-polyfills": "^1.0",
        "yoast/wp-test-utils": "^1.1",
        "phpunit/phpunit": "^9.6"
    }
}
# package.json
{
  "devDependencies": {
    "@wordpress/env": "^8.0.0"
  }
}

特に@wordpress/envはバージョンによってかなり違いがあり、例えばv5系辺りと執筆時時点で最新のv8系ではコマンドも違ってくるので注意してください。

手順

それではテスト実行の準備をしていきます。

ファイル構成

最初にファイル構成を確認しておきます。

/
├── .wp-env.json
├── composer.json
├── composer.lock
├── /node_modules
│   └── /.bin
│       └── wp-env
├── example-plugin.php
├── package-lock.json
├── package.json
├── phpunit.xml
├── /tests
│   ├── bootstrap.php
│   └── class-example-class-test.php
└── /vendor
    └── /bin
        └── phpunit

パッケージインストール

まずはパッケージ関連のインストール。

# プロジェクトルートに移動
$ cd your-project-root-directory

# @wordpress/envパッケージ導入
$ npm i -D @wordpress/env

# PHPUnit本体、yoastのパッケージ導入
composer require --dev phpunit/phpunit yoast/phpunit-polyfills yoast/wp-test-utils

# testsディレクトリ作成
mkdir tests

# 必要なファイルの作成
# bootstrap.phpはテスト実行前にロードされるファイル、WordPressの設定等を読み込みする
# phpunit.xmlはPHPUnitの設定ファイル
# .wp-env.jsonはwp-envの設定ファイル、頭の.を忘れないこと
touch tests/bootstrap.php phpunit.xml .wp-env.json example-plugin.php

コマンドの設定

実際にテストを行うには以下のようなコマンドを使います。

$ node_modules/.bin/wp-env run --env-cwd='wp-content/plugins/your-project-root-directory' tests-cli vendor/bin/phpunit -c phpunit.xml --testdox

これを毎度打つのは面倒なのでpackage.jsonのscriptsに登録して使えるようにします。

"scripts": {
  "wp-env": "wp-env",
  "test:env": "wp-env run --env-cwd='wp-content/plugins/your-project-root-directory' tests-cli vendor/bin/phpunit -c phpunit.xml --testdox --verbose"
},

これでnpm run test:envでテストを実行できるようになりました。

--env-cwdオプションは多分カレントワーキングディレクトリ(Current Working Directory)の略だと思うんですが、これを指定することでカレントディレクトリが指定したディレクトリとなってpath指定が楽になります。

ついでにwp-envも設定しておきましょう、node_modules/.bin/wp-envと打たなくても良くなります。

bootstrap.phpの記述

<?php

use Yoast\WPTestUtils\WPIntegration;

require_once dirname( __DIR__ ) . '/vendor/yoast/wp-test-utils/src/WPIntegration/bootstrap-functions.php';

$_tests_dir = WPIntegration\get_path_to_wp_test_dir();

if ( ! $_tests_dir ) {
	exit( PHP_EOL . "\033[41mWP_TESTS_DIR environment variable is not defined.\033[0m" . PHP_EOL . PHP_EOL );
}

require_once $_tests_dir . 'includes/functions.php';

/**
 * Manually load the Example Plugin.
 */
function _manually_load_plugin() {
	// example-plugin.phpは自分のプロジェクトのルートファイル、プラグインなら通常プラグイン名のファイルを指定
	require dirname( dirname( __FILE__ ) ) . '/example-plugin.php';
}
tests_add_filter( 'plugins_loaded', '_manually_load_plugin' );

WPIntegration\bootstrap_it();

phpunit.xmlの記述

<phpunit bootstrap="tests/bootstrap.php" colors="true">
    <testsuites>
        <testsuite name="Plugin Tests">
            <!--
                WordPressコーディング規約で定められたスネークケースのファイル名でテストを実行できるようにsuffixを指定
                PHPUnit標準のアッパーキャメルケースでテストファイルを作成する場合suffixは削除する
            -->
            <directory suffix="-test.php">tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

.wp-env.jsonの記述

  • 必ずカレントディレクトリをアクティベートするようにする、今回はプラグインなので配列に自分自身を表す"."を指定
  • portとtestsPortはそれぞれデフォルトが8888と8889、デフォルトで良ければ削除
{
	"config": {},
	"core": null,
	"env": {},
	"mappings": {},
	"phpVersion": null,
	"plugins": [
        	"."
	],
	"port": 40010,
	"testsPort": 40011,
	"themes": []
}

プラグインファイル作成

とりあえず動けばOKの簡単なプラグインを制作します。

<?php
/**
 * Plugin Name: Example Plugin
 */

class Example_Plugin {
    public function set_my_option(): bool {
        return update_option( 'my_option', 'my_value' );
    }
}

このプラグインはset_my_optionメソッドでWordPressのupdate_optionを使ってmy_optionというカラムを登録するという全く実用性がないプラグインです。

しかもインスタンス化も何もしていないのでアクティベートされただけでは本当に何もしない役に立たないプラグインです。

テストファイル作成

とりあえずまずは適当なテストを1つだけ書いて動作チェックを行います。

# テストファイル作成
$ touch tests/class-example-class-test.php
<?php

// composerでインストールしたYoastのTestCaseを使う
use Yoast\WPTestUtils\WPIntegration\TestCase;

class Example_Class_Test extends TestCase {
    public function set_up(): void {
        // 親のset_upメソッドをコール
        // Yoastのset_upメソッドはWP_UnitTestCaseのsetUp()のラッパー
        parent::set_up();
    }

    // 通常のPHPUnitと同じく、メソッド名はtest_から始める
    public function test_true_equal_true() {
        // true === trueのテストなので100%成功するテスト
        $this->assertTrue( true );
    }
}

wp-env start

必要なファイルを作成できたのでwp-envを実行します。

 npm run wp-env start

> wp-env
> wp-env start

WordPress development site started at http://localhost:40010
WordPress test site started at http://localhost:40011
MySQL is listening on port 53529
MySQL for automated testing is listening on port 53714

 ✔ Done! (in 62s 580ms)

問題なくDone!が出れば仮想環境ができました。

実際にURLにアクセスするとWordPressが開くと思います、/wp-admin/にアクセスするともちろんログインも可能。

wp-envのデフォルトユーザーはadmin、パスワードはpasswordでログインできます。

テスト実行

$ npm run test:env

> example-tests@1.0.0 test:env
> wp-env run --env-cwd='wp-content/plugins/example-tests' tests-cli vendor/bin/phpunit -c phpunit.xml --testdox

ℹ Starting 'vendor/bin/phpunit -c phpunit.xml --testdox' on the tests-cli container. 

Installing...
Running as single site... To run multisite, use -c tests/phpunit/multisite.xml
Not running ajax tests. To execute these, use --group ajax.
Not running ms-files tests. To execute these, use --group ms-files.
Not running external-http tests. To execute these, use --group external-http.
PHPUnit 9.6.8 by Sebastian Bergmann and contributors.

Example_Class_
 ✔ True equal true

Time: 00:00.023, Memory: 36.50 MB

OK (1 test, 1 assertion)

無事テストが成功しましたね!

PHPUnitでWordPressの関数を使ってテスト

それでは今度は通常のPHPUnit単体では不可能なWordPressの関数を使ったテストを実施します。

先程のテストファイルを編集します。

<?php

use Yoast\WPTestUtils\WPIntegration\TestCase;

class Example_Class_Test extends TestCase {
    private $instance;

    public function set_up(): void {
        parent::set_up();

        // テスト対象のクラスをインスタンス化して$this->instanceに格納
        $this->instance = new Example_Plugin();
    }

    public function test_set_my_option_before_execute() {
        // プラグイン実行前は当然my_optionカラムが存在しないのでfalseが返ってくることを確認
        $this->assertFalse( get_option( 'my_option' ) );
    }

    public function test_set_my_option_after_execute() {
        // set_my_optionメソッドを実行
        $this->instance->set_my_option();

        // プラグインが実行されたのでmy_optionカラムの値が'my_value'と同一であることを確認
        $this->assertSame( 'my_value', get_option( 'my_option' ) );
    }
}

このテストではWordPressのget_option関数を使用しているため、PHPUnit単体環境では実施できずWordPressの実行環境が必要になります。

そのためこのテストがうまく行けばきちんとWordPress環境下のテストが実施できているということになります。

それでは実行してみましょう!

$ npm run test:env

> example-tests@1.0.0 test:env
> wp-env run --env-cwd='wp-content/plugins/example-tests' tests-cli vendor/bin/phpunit -c phpunit.xml --testdox

ℹ Starting 'vendor/bin/phpunit -c phpunit.xml --testdox' on the tests-cli container. 

Installing...
Running as single site... To run multisite, use -c tests/phpunit/multisite.xml
Not running ajax tests. To execute these, use --group ajax.
Not running ms-files tests. To execute these, use --group ms-files.
Not running external-http tests. To execute these, use --group external-http.
PHPUnit 9.6.8 by Sebastian Bergmann and contributors.

Example_Class_
 ✔ Set my option before execute
 ✔ Set my option after execute

Time: 00:00.021, Memory: 36.50 MB

OK (2 tests, 3 assertions)

どちらのテストも通りましたね!

ちなみにテスト環境は実施した後毎回初期化されるので、次回実施時もtest_set_my_option_before_executeメソッド(プラグイン実行前は値が存在しないことを確認するテスト)はきちんとassertFalsetrueになります。

WordPress&PHPUnit&wp-envの解説

これ以降は詳細な解説です。

各種パッケージ

まずは各種パッケージについて。

@wordpress/wp-env

WordPress公式のパッケージで、リポジトリはWordPress/gutenbergです。

詳しい解説はブロックエディターハンドブックで見れます。

コマンドさえ覚えてしまえば結構柔軟に環境構築できるので、Local(旧Local by flywheel)等を使っている方でも移行の価値あり。

ただしデフォルトではphpMyAdminやAdminerコンテナが無いためプラグインで対応する等が必要ですし、MailHog機能も無いのでその辺の機能周りが必要なプロジェクトの場合別途対応する必要があります。

wp-envにAdminer+MailHogを組み合わせる方法を書きました、ぜひご覧ください。

yoast/PHPUnit-Polyfills

yoast/PHPUnit-Polyfillsパッケージはcomposerで提供されるポリフィルです。

察しの言い方はご存知かもしれませんがあのYoast SEOというメジャープラグインを制作しているyoastのパッケージです。

主にバージョン差異を埋めるために必要なのがポリフィルですが、2021年の公式の記事にで今後はPHPUnit-Polyfillsが必須であると記載があります。

最初僕がWordPress環境下のPHPUnit実行について調べ始めた時は日本語情報がほとんど無い中以下の記事でPHPUnit-Polyfillsについて知って大変助かりました。

yoast/wp-test-utils

こちらも同じくyoastが制作するWordPressテスト用のユーティリティパッケージです。

bootstrap.phpで実行する便利な関数や、WP_UnitTestCaseを継承するTestCaseを使用したりと無くても動くけどあればめちゃくちゃ便利なパッケージ。

今回は取り扱いませんが他のテストパッケージとの連携だったり便利なモックの提供だったりと便利機能がたくさんあるのでぜひyoast/wp-test-utilsのリポジトリもチェックしてみてください。

設定ファイル

次は設定ファイルをチェックしていきます。

tests/bootstrap.php

<?php

// yoast/wp-test-utilsのWPIntegrationを使用
use Yoast\WPTestUtils\WPIntegration;

// yoast/wp-test-utilsのWPIntegrationのbootstrap_it()を使用ためファイルを読み込む
require_once dirname( __DIR__ ) . '/vendor/yoast/wp-test-utils/src/WPIntegration/bootstrap-functions.php';

/**
 * get_path_to_wp-test_dir関数はまずwp-envの環境変数WP_TESTS_DIRをチェック
 * 通常npm run wp-env start時にWP_TESTS_DIRが設定されるのでこの値を$_tests_dirに格納
 * 環境変数を上書きしていなければWP_TESTS_DIR=/wordpress-phpunit
 * なおテスト環境の環境変数は以下のコマンドで確認可能
 * 
 * $ npm run wp-env run tests-cli -- printenv
 * 
 * もしWP_TESTS_DIRが設定されていなければWP_DEVELOP_DIRをチェック
 * デフォルトではWP_DEVELOP_DIRは存在しないので必要な場合はWP_DEVELOP_DIRを設定すればこちらでも対応できる
 * 
 * WP_TESTS_DIRもWP_DEVELOP_DIRも設定されていなければWordPressコアのWP_CONTENT_DIRとABSPATHを使用して
 * プラグインディレクトリへのアクセスを試みる
 * 
 * 実装はリポジトリをチェック
 * https://github.com/Yoast/wp-test-utils/blob/12ad78c4daca79342d9f6a0056c560e3f48cf0dc/src/WPIntegration/bootstrap-functions.php#L37
 */
$_tests_dir = WPIntegration\get_path_to_wp_test_dir();

// $_tests_dirが設定されていなければエラーを出力して終了
if ( ! $_tests_dir ) {
	exit( PHP_EOL . "\033[41mWP_TESTS_DIR environment variable is not defined.\033[0m" . PHP_EOL . PHP_EOL );
}

/**
 * ファイルの実体は以下で確認可能
 * 
 * $ npm run wp-env run tests-cli -- cat /wordpress-phpunit/includes/functions.php
 * 
 * この後のtests_add_filter関数を利用するのに必要
 */
require_once $_tests_dir . 'includes/functions.php';

/**
 * プラグインをWordPressにフックするための関数
 * 中身はadd_filterのラッパー
 */
function _manually_load_plugin() {
	require dirname( dirname( __FILE__ ) ) . '/example-plugin.php';
}
tests_add_filter( 'plugins_loaded', '_manually_load_plugin' );

/**
 * yoast/wp-test-utilsの関数を実行
 * 
 * 内部でyoast/PHPUnit-Polyfillsのphpunitpolyfills-autoload.phpを呼び出している
 * また、wp-envのWP_TESTS_DIR配下のbootstrap.phpの呼び出しも行っている。
 * 実装は以下のコマンドで確認可能
 * 
 * $ npm run wp-env run tests-cli -- cat /wordpress-phpunit/includes/bootstrap.php
 */
WPIntegration\bootstrap_it();

少し長くなってしまいましたが大体こんな手順で処理されています。

yoast/wp-test-utilsを使わない場合yoast/PHPUnit-Polyfillsのオートローダーを書く必要があるんですが、yoast/wp-test-utilsでうまい具合に調整してくれてるので読み込んで関数実行で行くようになっています。

ちなみに僕はファイルの末尾にこんな定数を追加で宣言してます。

define( 'ROOT_DIR', dirname( dirname( __FILE__ ) ) );

こんな感じでbootstrap内で定数を設定しておくと、testsディレクトリ配下の全てのファイルで定数が使い回せるのでプラグインのルートディレクトリを定数化しておくと結構便利なんですよね。

他にもテストでよく使いたい値があれば設定できるので「これ何回も書いてるな」という方は定義しておくと良さそうです。

VSCode等のIDEを使っていると、上述のtests_add_filter関数やWP_UnitTestCaseのメソッドが定義されていないと怒られることがあります。

そんな時はcomposerでphp-stubs/wordpress-tests-stubsパッケージをインストールしておきましょう。

各種メソッドの定義が書かれているのでIDEの未定義エラーを一掃することができます。

$ composer require --dev php-stubs/wordpress-tests-stubs

パッケージインストール後ウインドウ再起動の必要があるので一度閉じて開き直すか、cmd+shift+pでコマンドパレットを開いて>Developer: Reload Windowを実行すればエラーが消えます。

phpunit.xml

<phpunit bootstrap="tests/bootstrap.php" colors="true">
    <testsuites>
        <testsuite name="Plugin Tests">
            <directory suffix="-test.php">tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

あまり説明することもないですが、bootstrapの指定と必要であればsuffixの指定を忘れないようにしてください。

実際にはテストが膨大になってきたらtestsuiteを複数定義していい感じにテストを実行できるようにしたり、特定のテストを除外したりと運用が長くなればなるほどお世話になる回数が増えるファイルです。

.wp-env.json

{
	"config": {},
	"core": null,
	"env": {},
	"mappings": {},
	"phpVersion": null,
	"plugins": [
        "."
	],
	"port": 40010,
	"testsPort": 40011,
	"themes": []
}

wp-envの設定ファイルですが、wp-envのバージョンアップに伴って書き方がちょこちょこ変わります。

もし依存するプラグインやテーマがあればここに書いておきましょう。

ちなみに僕はこんな感じのほぼブランク状態の.wp-env.jsonでGitHub管理して、実際には.wp-env.override.jsonを.gitignoreに書いて使っています。

.wp-env.override.jsonは.wp-env.jsonより優先されるため、手元の環境を柔軟に変更できます。

仮にプロジェクトがフォークされた場合、「この設定だけは絶対に使って!」という設定だけを書いて後は自分で.wp-env.override.jsonで設定するというような方式が良さそうですね。

テストファイル

テストファイルも簡単に解説します。

<?php

use Yoast\WPTestUtils\WPIntegration\TestCase;

class Example_Class_Test extends TestCase {
    private $instance;

    public function set_up(): void {
        parent::set_up();

        // テスト対象のクラスをインスタンス化して$this->instanceに格納
        $this->instance = new Example_Plugin();
    }

    public function test_set_my_option_before_execute() {
        // プラグイン実行前は当然my_optionカラムが存在しないのでfalseが返ってくることを確認
        $this->assertFalse( get_option( 'my_option' ) );
    }

    public function test_set_my_option_after_execute() {
        // set_my_optionメソッドを実行
        $this->instance->set_my_option();

        // プラグインが実行されたのでmy_optionカラムの値が'my_value'と同一であることを確認
        $this->assertSame( 'my_value', get_option( 'my_option' ) );
    }
}

通常PHPUnitではアッパーキャメルケースが採用されているので、本来setUpなんですがWordPressコーディング規約ではメソッド名はスネークケースが採用されているのでそれに習ってスネークケースで記述しています。

親のparent::set_up()で指定しているyoastのTestCaseもスネークケースなので、コーディング規約を導入しないのは現代プロジェクトはありえないですし統一しておきましょう。

  • assertSame
  • assertTrue
  • assertFalse

この辺のお馴染みのメソッドはもちろん、WordPressのメソッドも使用できます。

  • go_to
  • factory

WP_UnitTestCaseのアサーションやユーティリティメソッドはwordpress-develop/tests/phpunit/includesで見ることができます。

例えばgo_toメソッドはabstract-testcase.phpに実装されています。

WordPress&PHPUnitのモダンユニットテスト環境をwp-envを使って構築する まとめ

これらの情報は正直一つにまとまっているとは言いづらく、本来なら公式リファレンスできちんと記述しておいてほしい情報ばかりでした。

WordPressのプロジェクトはまだまだテストやコーディング規約の採用と言ったモダン開発を採用していないプロジェクトもかなり見かけるんですが、採用すると本当に便利で余計なミスや思考が減るのでぜひ採用して楽しいデベロッパーライフを送りましょう!

分からない点、レガシーな点、その他「もっとこういう方法あるよ!」という声はぜひコメントで頂けると僕も勉強になるのでどんどんお寄せください!

スポンサーリンク
WordPress
スポンサーリンク
\この記事いいね!と思ったらシェアしてね/
スポンサーリンク
L'7 Records

コメント

タイトルとURLをコピーしました