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.distconvertNoticesToExceptionsconvertWarningsToExceptionsを状況に応じてfalseに設定することも検討してください

まとめ

PHPUnitとWP_UnitTestCaseを使えば、WordPressプラグインのビジネスロジックからフック登録まで自動テストできます。GitHub Actionsと組み合わせることでPRのたびに自動テストが走り、品質を継続的に保証できます。最初は既存コードのテストから始め、徐々にTDDの習慣を身につけましょう。

関連記事:GitHub ActionsでWordPressのCI/CDパイプラインを構築する方法

お気軽にご相談ください

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