ユニットテストについて
近年のプログラミングの現場ではユニットテストは当たり前のように求められ、コードのデプロイのサイクルにも取り入れられているものと思います。
これはプログラミング言語を問わず言えることでしょう。
WordPressの場合はどうなのか
一方でWordPressを用いたWebサイト制作においては、ユニットテストの積極的にやっているケースは少ないように思います。
多くのWordpressサイト構築を引き継いできましたが、これまでの経験でテストコードが存在するようなものはありませんでした。
これまで社内でWordPressサイトを構築する場合もtheme-test-data-jaやE2Eテストのようなものは状況に応じて実施するものの、ユニットテストのコード作成は行ってきませんでした。
それでは、WordPressサイト構築においても、PHPUnitでテストしたいとなった場合はどのようにすれば良いでしょうか。
有名どころの言語やフレームワークなどにおいてはユニットテストについて十分な情報が提供されていますが、WordPress独自テーマ作成によるWebサイト構築においては、具体的な最適解が分かりにくい傾向にあります。
そもそも、WordPressサイトの場合は中核となる業務領域を担う(すなわち複雑な業務ロジックの実装が)必要がなく、あくまでプレゼンテーション層レベルの話や単純なCRUDで完結するため、ユニットテストの重要性が求められないということもあるかもしれません。
どちらかというと、機能面よりもデザイン通りに実装されているかの確認の方が関心の対象となるでしょう。
しかし、WordPressと言えどもプログラムの塊であることには違いありませんし、日々の運用においてテストの仕組みが整備されていないと常に既存の機能を壊す心配を抱えることになり、デプロイの心理的負荷は上がります。
この文書では、WordPressの独自テーマ開発においてのPHPUnitの導入と具体的なテストコードについて記載したいと思います。
使用技術
調べたところ、2024年8月現在の標準的なWordPressでのユニットテストの手法としては下記が良さそうです。
- 動作環境にはWordPress標準制作フロー2024でも紹介した
@wordpress/env
のテスト用インスタンスを利用 - テストケースにはYoast/wp-test-utilsを利用
導入手順
@wordpress/env
まずは、@wordpress/env
の導入します。
npm init
npm i @wordpress/env --save-dev
.wp-env.json
を設置します。
ここでは、WP_DEBUG
をtrue
にするのと、マッピングの設定のみを行いました。
(data
ディレクトリは中にカバレッジレポートのHTMLを出力するために作成しています。)
{
"config": {
"WP_DEBUG": true
},
"mappings": {
"../data": "./data",
"wp-content/themes": "./themes"
}
}
wp-env
を開始しますが、XDebugを有効にするには1下記のように--xdebug
オプションを付加する必要があります。
npx wp-env start --xdebug
テーマファイルを作成
WP CLIを用いてUnderscoresを雛形としたテーマを作成します。
npx wp-env run cli wp scaffold _s sample-theme
作成したテーマへの切り替えとComposerで定義されたライブラリのインストールとPHPUnitの導入を行いましょう。
npx wp-env run cli wp theme activate sample-theme
npx wp-env run cli --env-cwd=wp-content/themes/sample-theme/ composer update
npx wp-env run cli --env-cwd=wp-content/themes/sample-theme/ composer require --dev yoast/wp-test-utils phpunit/phpunit --with-dependencies
PHPUnitの関連ファイル作成
themes/sample-theme/phpunit.xml.dist
を設置します。
<?xml version="1.0"?>
<phpunit bootstrap="_tests/bootstrap.php" colors="true">
<coverage>
<include>
<directory suffix=".php">./</directory>
</include>
<exclude>
<directory suffix=".php">./vendor/</directory>
<directory suffix=".php">./_tests/</directory>
</exclude>
<report>
<text outputFile="php://stdout" showOnlySummary="true" />
</report>
</coverage>
<testsuites>
<testsuite name="testing">
<directory suffix="-test.php">./_tests/</directory>
</testsuite>
</testsuites>
<logging />
</phpunit>
themes/sample-theme/_tests/bootstrap.php
を設置します。
active_plugins
でテスト時に有効となっている必要があるプラグインを定義します。
ここではAdvanced Custom Fieldsを追加しています。
<?php
use Yoast\WPTestUtils\WPIntegration;
$GLOBALS['wp_tests_options'] = array(
'active_plugins' => array(
'advanced-custom-fields-pro/acf.php',
),
);
require_once dirname( __DIR__ ) . '/vendor/yoast/wp-test-utils/src/WPIntegration/bootstrap-functions.php';
WPIntegration\bootstrap_it();
switch_theme( 'sample-theme' );
簡単にテストを実行できるようにcomposer.json
のscripts
に下記を追加します。
"test": "XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-html /var/www/data/_coverage-html"
これにより、下記のようにテストを実行することができます。
npx wp-env run tests-cli --env-cwd=wp-content/themes/sample-theme/ composer test
まだコマンドが長すぎますか?
composer.json
のscripts
で定義したことで、tests-cli
インスタンス上のwp-content/themes/sample-theme/
へ移動してcomposer test
するだけに簡略化できましたが、この処理を一つのコマンドにするとまだ長すぎると感じるかもしれません。
その場合は、package.json
のscripts
も併せて定義すると良いでしょう。
"test": "wp-env run tests-cli --env-cwd=wp-content/themes/sample-theme/ composer test"
これでテストの実行はnpm run test
で呼び出せるようになりました。
テストコード例
このようなテストコードとなります。
themes/sample-theme/_tests/class-basic-test.php
<?php
use Yoast\WPTestUtils\BrainMonkey\TestCase;
class BasicTest extends TestCase {
protected function set_up() {
parent::set_up();
}
public function testThemeName() {
$my_theme = wp_get_theme();
$this->assertEquals( 'Sample-theme', $my_theme->get( 'Name' ) );
}
}
もし、functions.php
でカスタマイズされた機能や関数をテストする場合は、set_up
の中で読み込むと良いでしょう。
例えば、下記のようなコードの場合です。
テスト対象となるコード:
// Yoast SEOのパンくずをカスタマイズ
function my_wpseo_breadcrumb_links( $breadcrumbs ) {
// 具体的な実装は省略
}
add_filter( 'wpseo_breadcrumb_links', 'my_wpseo_breadcrumb_links', 10, 1 );
テストコード:
<?php
use Yoast\WPTestUtils\BrainMonkey\TestCase;
class WpseoTest extends TestCase {
protected function set_up() {
parent::set_up();
require_once get_template_directory() . '/functions.php';
}
public function testWpseo() {
// ここでは簡易的な例示のため配列かどうかのみをテストしていますが
// 実際にはもう少し複雑なテストになるでしょう。
$after_breadcrumbs = my_wpseo_breadcrumb_links( array() );
$this->assertIsArray( $after_breadcrumbs );
}
}
テンプレートファイルのテスト
functions.php
で定義されているような関数のテストについては上記のやり方で十分でしょう。一方で、archive.php
やsingle.php
のようなテーマファイルのテストも自動化したくなります。
(厳密にいうと、機能単体のテストではなくなるので、統合テストに近いものとなりますが)
Underscoresのarchive.php
テンプレートを例にテストを記載してみましょう。
テストコード:
themes/sample-theme/_tests/class-archive-test.php
<?php
use Yoast\WPTestUtils\BrainMonkey\TestCase;
class ArchiveTest extends TestCase {
protected function set_up() {
parent::set_up();
require_once get_template_directory() . '/functions.php';
}
public function testEmptyPostsArchivePage() {
global $wp_query;
$wp_query = new WP_Query( array( 'post_type' => 'post' ) );
ob_start();
include get_template_directory() . '/archive.php';
$output = ob_get_clean();
$this->assertDoesNotMatchRegularExpression( '/error/i', $output );
}
public function testHasPostsArchivePage() {
global $wp_query;
// Generate dummy data.
for ( $i = 1; $i <= 5; $i++ ) {
wp_insert_post(
array(
'post_title' => 'SamplePost' . $i,
'post_type' => 'post',
'post_status' => 'publish',
)
);
}
$wp_query = new WP_Query( array( 'post_type' => 'post' ) );
ob_start();
include get_template_directory() . '/archive.php';
$output = ob_get_clean();
$this->assertDoesNotMatchRegularExpression( '/error/i', $output );
}
}
カバレッジレポート:
data/_coverage-html/archive.php.html
WPTestUtils
は実行時にデータがリセットされるので必要に応じてwp_insert_post
などでデータを追加してからテストを行っているのと、必要に応じて$wp_query
などのグローバル変数の書き換えがポイントとなります。
testEmptyPostsArchivePage
ではif ( have_posts() ) :
がfalse
となるテスト、testHasPostsArchivePage
ではtrue
となるテストを記載しています。
ここでは単純に出力HTMLにerror
という文字列が含まれていないか確認していますが、実際には想定した数の要素が出ているかなどをテストすると良いでしょう。
次に、single.php
テンプレートのテスト例を記載します。
themes/sample-theme/_tests/class-single-test.php
<?php
use Yoast\WPTestUtils\BrainMonkey\TestCase;
class SingleTest extends TestCase {
protected function set_up() {
parent::set_up();
require_once get_template_directory() . '/functions.php';
}
public function testSinglePost() {
global $wp_query;
$insert_data = array(
'post_title' => 'SamplePost',
'post_type' => 'post',
'post_status' => 'publish',
);
$wp_insert_post = wp_insert_post( $insert_data );
$wp_query = new WP_Query( array( 'post_id' => $wp_insert_post ) );
ob_start();
include get_template_directory() . '/single.php';
$output = ob_get_clean();
$this->assertDoesNotMatchRegularExpression( '/error/i', $output );
$removed_line_breaks = str_replace( array( "\r\n", "\r", "\n" ), '', $output );
$this->assertMatchesRegularExpression( '/<h2[^>]*>.*>' . $insert_data['post_title'] . '<.*<\/h2>/', $removed_line_breaks );
}
public function testSinglePage() {
global $wp_query;
$insert_data = array(
'post_title' => 'SamplePage',
'post_type' => 'page',
'post_status' => 'publish',
);
$wp_insert_post = wp_insert_post( $insert_data );
$wp_query = new WP_Query( array( 'page_id' => $wp_insert_post ) );
ob_start();
include get_template_directory() . '/single.php';
$output = ob_get_clean();
$this->assertDoesNotMatchRegularExpression( '/error/i', $output );
$removed_line_breaks = str_replace( array( "\r\n", "\r", "\n" ), '', $output );
$this->assertMatchesRegularExpression( '/<h1[^>]*>\s*' . $insert_data['post_title'] . '\s*<\/h1>/', $removed_line_breaks );
}
}
このテストでは、投稿と固定ページのダミーデータを作成し、HTML上にerror
という文字列が含まれていないかと、見出しタグに想定どおりのタイトルが入っているかを確認しています。
まとめ
Symfonyなどのフレームワークでのユニットテストに慣れていると、グローバス変数を上書きしたり、投入データがフィクスチャとしてうまく分離できていなかったりと少し違和感がありますが、色々と手を動かした結果このような落としどころになりました。
もう少し良い方法が見つかればまた記事としてまとめたいと思います。
参考情報
-
XDebugを有効にしているのはコードカバレッジレポートを出力するためです。 ↩