2026年5月21日
2026年5月21日
PHPUnitを使ったWordPressプラグインのユニットテスト実装方法
はじめに
WordPressプラグインの品質を保証するにはユニットテストが不可欠です。PHPUnitとWordPress独自のWP_UnitTestCaseを組み合わせることで、投稿作成・フック登録・データベース処理まで自動テストできます。TDD(テスト駆動開発)の手法でバグを早期発見し、リファクタリングへの恐怖をなくしましょう。
症状・原因
- プラグインを修正するたびに別の機能が壊れないか不安で変更できない
- 手動テストに時間がかかりすぎてリリースサイクルが遅くなっている
- フックやフィルターが正しく登録・実行されているか確認する方法がわからない
- WordPressのグローバル関数をモックしてテストする方法がわからない
解決手順
ステップ1:テスト環境のスキャフォールドとセットアップ
WP-CLIのscaffoldコマンドでテスト環境を自動生成します。
# wp-cli/wp-cli-bundle がインストール済みであることを確認
wp --version
# プラグインのテストスキャフォールドを生成
wp scaffold plugin-tests my-plugin \
--dir=/var/www/html/wp-content/plugins/my-plugin
# 生成されるファイル構成:
# tests/
# bootstrap.php — テスト初期化
# test-sample.php — サンプルテスト
# phpunit.xml.dist — PHPUnit設定
# bin/
# install-wp-tests.sh — WPテストスイートのインストールスクリプト
# WordPressのテストスイートをインストール
# (テスト用DBが必要 — 本番DBとは別に作成すること!)
bash bin/install-wp-tests.sh \
wordpress_test \
root \
password \
localhost \
latest
# PHPUnitのインストール(Composerで)
composer require --dev phpunit/phpunit:"^10.0"
composer require --dev wp-phpunit/wp-phpunit
# PHPUnitが使えることを確認
./vendor/bin/phpunit --version
ステップ2:phpunit.xml.dist設定とbootstrap.phpのセットアップ
<!-- phpunit.xml.dist -->
<?xml version="1.0"?>
<phpunit
bootstrap="tests/bootstrap.php"
backupGlobals="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
>
<testsuites>
<testsuite name="My Plugin Test Suite">
<directory suffix="Test.php">./tests/unit</directory>
<directory suffix="Test.php">./tests/integration</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./src</directory>
</include>
<report>
<html outputDirectory="coverage"/>
<clover outputFile="coverage.xml"/>
</report>
</coverage>
<php>
<env name="WP_PHPUNIT__TESTS_CONFIG" value="tests/wp-tests-config.php"/>
</php>
</phpunit>
<?php
// tests/bootstrap.php
// Composerのオートロードを読み込む
require_once dirname( __DIR__ ) . '/vendor/autoload.php';
// WordPressテストスイートのディレクトリを設定
$_tests_dir = getenv( 'WP_TESTS_DIR' ) ?: '/tmp/wordpress-tests-lib';
if ( ! file_exists( $_tests_dir . '/includes/functions.php' ) ) {
echo "WPテストスイートが見つかりません: {$_tests_dir}\n";
exit( 1 );
}
// プラグインのロードを登録
tests_add_filter( 'muplugins_loaded', function() {
require dirname( __DIR__ ) . '/my-plugin.php';
} );
// WordPressのテストスイートをブートストラップ
require $_tests_dir . '/includes/bootstrap.php';
ステップ3:WP_UnitTestCaseでテストを書く
WP_UnitTestCaseはPHPUnitのTestCaseを拡張し、WordPress固有のヘルパーを提供します。
<?php
// tests/unit/PostServiceTest.php
use MyPlugin\Services\PostService;
use MyPlugin\Repositories\PostRepository;
class PostServiceTest extends WP_UnitTestCase {
private PostService $service;
/**
* 各テストの前に実行される
*/
public function setUp(): void {
parent::setUp();
$repository = new PostRepository();
$this->service = new PostService( $repository );
}
/**
* 各テストの後に実行される(DBは自動的にロールバック)
*/
public function tearDown(): void {
parent::tearDown();
}
/**
* ファクトリメソッドで投稿を作成してテスト
*/
public function test_get_recent_posts_returns_published_only(): void {
// Arrange: ファクトリで投稿を作成
$published_id = self::factory()->post->create([
'post_status' => 'publish',
'post_title' => '公開済み投稿',
]);
self::factory()->post->create([
'post_status' => 'draft',
'post_title' => '下書き投稿',
]);
// Act: テスト対象のメソッドを実行
$posts = $this->service->get_recent_posts( 10 );
// Assert: 公開済みのみ返されることを確認
$this->assertCount( 1, $posts );
$this->assertEquals( $published_id, $posts[0]->ID );
}
/**
* カスタムポストタイプのファクトリ使用例
*/
public function test_create_product_saves_price_meta(): void {
// カスタム投稿タイプの投稿を作成
$product_id = self::factory()->post->create([
'post_type' => 'product',
]);
// メタデータを更新
$this->service->update_price( $product_id, 1500 );
// メタデータが保存されていることを確認
$price = get_post_meta( $product_id, '_price', true );
$this->assertEquals( 1500, (int) $price );
}
/**
* ユーザーファクトリの使用例
*/
public function test_only_admin_can_delete_product(): void {
$admin_id = self::factory()->user->create([
'role' => 'administrator',
]);
$editor_id = self::factory()->user->create([
'role' => 'editor',
]);
$product_id = self::factory()->post->create([
'post_type' => 'product',
]);
wp_set_current_user( $editor_id );
$this->assertFalse( $this->service->can_delete( $product_id ) );
wp_set_current_user( $admin_id );
$this->assertTrue( $this->service->can_delete( $product_id ) );
}
}
ステップ4:フック・アクションのテスト
has_filter()とdid_action()を使ったフックのテスト方法です。
<?php
// tests/unit/HooksTest.php
class HooksTest extends WP_UnitTestCase {
/**
* プラグインが正しいフックに登録されているか確認
*/
public function test_hooks_are_registered_on_init(): void {
// プラグインクラスを初期化
$plugin = new My_Plugin();
// フィルターが登録されているか確認
$this->assertTrue(
has_filter( 'the_content', [ $plugin, 'filter_content' ] ) !== false,
'the_content フィルターが登録されていません'
);
// 特定のpriorityで登録されているか確認
$priority = has_action( 'save_post', [ $plugin, 'save_product_meta' ] );
$this->assertEquals( 20, $priority, 'save_post のpriorityが20でありません' );
}
/**
* カスタムアクションが発火するか確認
*/
public function test_custom_action_fires_on_product_creation(): void {
// アクション発火を確認するためのモックコールバック
$action_fired = false;
$fired_post_id = null;
add_action( 'my_plugin/product_created', function( $post_id ) use ( &$action_fired, &$fired_post_id ) {
$action_fired = true;
$fired_post_id = $post_id;
} );
// 商品を作成
$product_id = self::factory()->post->create([
'post_type' => 'product',
]);
do_action( 'save_post', $product_id, get_post( $product_id ), true );
// カスタムアクションが発火したか確認
$this->assertTrue( $action_fired );
$this->assertEquals( $product_id, $fired_post_id );
// did_action() で発火回数を確認
$this->assertEquals( 1, did_action( 'my_plugin/product_created' ) );
}
/**
* フィルターが値を正しく変換するか確認
*/
public function test_price_filter_formats_with_yen_symbol(): void {
// apply_filters で実際のフィルター実行をテスト
$raw_price = 1500;
$filtered_price = apply_filters( 'my_plugin/format_price', $raw_price );
$this->assertStringContainsString( '¥', $filtered_price );
$this->assertStringContainsString( '1,500', $filtered_price );
}
}
ステップ5:PHPUnitの実行とGitHub Actions連携
# テストを実行
./vendor/bin/phpunit
# カバレッジレポートを生成
./vendor/bin/phpunit --coverage-html coverage/
# 特定のテストクラスのみ実行
./vendor/bin/phpunit --filter PostServiceTest
# 特定のテストメソッドのみ実行
./vendor/bin/phpunit --filter test_get_recent_posts_returns_published_only
# .github/workflows/test.yml
name: WordPress Plugin Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: wordpress_test
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5
strategy:
matrix:
php: ['8.1', '8.2', '8.3']
wp: ['6.4', '6.5', 'latest']
steps:
- uses: actions/checkout@v4
- name: Setup PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: mysqli, pdo_mysql
coverage: xdebug
- name: Install Composer dependencies
run: composer install --no-progress --prefer-dist
- name: Install WordPress test suite
run: |
bash bin/install-wp-tests.sh \
wordpress_test root root localhost ${{ matrix.wp }}
- name: Run PHPUnit tests
run: ./vendor/bin/phpunit --coverage-clover coverage.xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
注意事項
bin/install-wp-tests.shでインストールするテスト用DBは本番データベースとは必ず別のものを使用してください。テスト実行後にDBの内容が変わる場合がありますWP_UnitTestCaseは各テストをトランザクション内で実行しロールバックしますが、wp_insert_post()などで発火するアクションの副作用(メール送信、外部APIコールなど)はロールバックされません。外部依存はモックするか、テスト環境で無効化してください- PHP 8.x系とWordPress 6.x系の組み合わせによっては非推奨警告が多数出る場合があります。
phpunit.xml.distのconvertNoticesToExceptionsやconvertWarningsToExceptionsを状況に応じてfalseに設定することも検討してください
まとめ
PHPUnitとWP_UnitTestCaseを使えば、WordPressプラグインのビジネスロジックからフック登録まで自動テストできます。GitHub Actionsと組み合わせることでPRのたびに自動テストが走り、品質を継続的に保証できます。最初は既存コードのテストから始め、徐々にTDDの習慣を身につけましょう。