2026年9月3日

2026年9月3日

WordPressプラグインにPHPUnitでユニットテストを追加する方法

はじめに

WordPressプラグインのコードにバグが多くユニットテストを追加したい・WP_UnitTestCaseの使い方がわからない・テスト用のWordPressインストールのセットアップがわからない・GitHub ActionsでCIパイプラインを構築したいといった問題の解決方法を解説します。

症状・原因

  • WordPress用のテスト環境(wp-tests-config.php)が設定されていない
  • WP_UnitTestCaseを継承しているが各テストの前後でDBがリセットされないと思い込んでいる
  • setUp()tearDown()の中でWordPressの状態が汚染されて後続テストが失敗する
  • モックオブジェクトの設定が間違っていてフックが正しくテストできない

解決手順

ステップ1:テスト環境をセットアップする

# ✅ プラグインディレクトリに移動
cd /var/www/html/wp-content/plugins/my-plugin

# ✅ WP-CLI でテストスキャフォールドを生成(推奨)
wp scaffold plugin-tests my-plugin --path=/var/www/html/
# → tests/
# →   bootstrap.php    ← テスト起動ファイル
# →   test-sample.php  ← サンプルテスト
# → phpunit.xml.dist   ← PHPUnit設定
# → bin/
# →   install-wp-tests.sh ← テストDB作成スクリプト

# ✅ テスト用データベースを作成してWordPressをインストール
bash bin/install-wp-tests.sh wordpress_test root password localhost latest
# → テスト用のWordPressがインストールされる
# → 通常のサイトとは別のDB(wordpress_test)を使用

# ✅ PHPUnit をインストール
composer require --dev phpunit/phpunit:^10 brain/monkey:^2.6

# ✅ phpunit.xml.dist の確認
cat phpunit.xml.dist
# → <testsuites>
# →   <testsuite name="WP Plugin Tests">
# →     <directory suffix="Test.php">./tests</directory>
# →   </testsuite>
# → </testsuites>

# ✅ テストを実行
./vendor/bin/phpunit
# → PHPUnit 10.x.x
# → . → テスト成功
# → 1 test, 1 assertion

ステップ2:WP_UnitTestCaseを使ったテストを作成する

// ✅ tests/test-plugin.php
class MyPluginTest extends WP_UnitTestCase {
    
    // ✅ 各テストの前に実行(DBはWP_UnitTestCaseが自動リセット)
    public function setUp(): void {
        parent::setUp();
        // テスト用のセットアップ
        update_option('my_plugin_enabled', true);
    }
    
    public function tearDown(): void {
        parent::tearDown();
        // テスト後のクリーンアップ
        delete_option('my_plugin_enabled');
    }
    
    // ✅ 投稿が正しく作成されるかテスト
    public function test_create_post(): void {
        $post_id = wp_insert_post([
            'post_title'  => 'Test Post',
            'post_status' => 'publish',
            'post_type'   => 'post',
        ]);
        
        $this->assertIsInt($post_id);
        $this->assertGreaterThan(0, $post_id);
        $this->assertEquals('Test Post', get_the_title($post_id));
    }
    
    // ✅ フックが正しく動作するかテスト
    public function test_hook_is_registered(): void {
        $plugin = new \MyCompany\MyPlugin\Plugin();
        $plugin->initialize();
        
        $this->assertEquals(10, has_action('init', [$plugin, 'setup']));
    }
    
    // ✅ オプションが正しく保存されるかテスト
    public function test_save_settings(): void {
        $settings = new \MyCompany\MyPlugin\Admin\Settings();
        $settings->save(['option_key' => 'test_value']);
        
        $this->assertEquals('test_value', get_option('my_plugin_option_key'));
    }
}

ステップ3:モックとスタブを使ったテスト

// ✅ Brain\Monkey を使ったWordPress関数のモック
use Brain\Monkey;
use Brain\Monkey\Functions;
use Brain\Monkey\Actions;

class MyPluginWithMockTest extends \PHPUnit\Framework\TestCase {
    
    protected function setUp(): void {
        parent::setUp();
        Monkey\setUp(); // ← Brain\Monkey の初期化
    }
    
    protected function tearDown(): void {
        Monkey\tearDown(); // ← 後始末
        parent::tearDown();
    }
    
    // ✅ WordPress 関数をモック(データベース不要でテスト可能)
    public function test_get_post_title_with_mock(): void {
        Functions\when('get_the_title')
            ->justReturn('Mocked Post Title');
        Functions\when('get_post')
            ->justReturn((object)['ID' => 1, 'post_title' => 'Mocked Post Title']);
        
        $my_class = new \MyCompany\MyPlugin\Frontend\PostHelper();
        $result = $my_class->getTitle(1);
        
        $this->assertEquals('Mocked Post Title', $result);
    }
    
    // ✅ フックが実行されたかテスト
    public function test_action_is_triggered(): void {
        Actions\expectDone('my_plugin_action')
            ->once()
            ->with('expected_argument');
        
        do_action('my_plugin_action', 'expected_argument');
    }
    
    // ✅ PHPUnit のモックオブジェクト
    public function test_api_call_with_mock(): void {
        $mock_http = $this->createMock(\MyCompany\MyPlugin\API\HttpClient::class);
        $mock_http->method('get')
                  ->willReturn(['status' => 200, 'data' => ['key' => 'value']]);
        
        $service = new \MyCompany\MyPlugin\API\DataService($mock_http);
        $result = $service->fetchData('endpoint');
        
        $this->assertEquals('value', $result['key']);
    }
}

ステップ4:データプロバイダーと例外テスト

// ✅ データプロバイダーで複数のケースをまとめてテスト
class ValidationTest extends WP_UnitTestCase {
    
    /**
     * @dataProvider validEmailProvider
     */
    public function test_valid_email(string $email, bool $expected): void {
        $validator = new \MyCompany\MyPlugin\Utils\Validator();
        $this->assertEquals($expected, $validator->isValidEmail($email));
    }
    
    public static function validEmailProvider(): array {
        return [
            'valid email'     => ['test@example.com', true],
            'invalid email'   => ['not-an-email', false],
            'empty string'    => ['', false],
            'japanese domain' => ['test@example.co.jp', true],
        ];
    }
    
    // ✅ 例外がスローされることをテスト
    public function test_throws_exception_for_invalid_post(): void {
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage('Invalid post ID');
        
        $handler = new \MyCompany\MyPlugin\Frontend\PostHelper();
        $handler->getTitle(0); // 無効なID
    }
    
    // ✅ DBトランザクションのテスト(ロールバック確認)
    public function test_order_creation_rolls_back_on_error(): void {
        $initial_count = $this->factory->post->count();
        
        try {
            (new \MyCompany\MyPlugin\Orders\OrderService())->createWithInvalidData([]);
        } catch (\Exception $e) {
            // 例外後もDBは変更されていないことを確認
            $this->assertEquals($initial_count, $this->factory->post->count());
        }
    }
}

ステップ5:GitHub ActionsでCIを設定する

# ✅ .github/workflows/tests.yml を作成
mkdir -p .github/workflows
cat > .github/workflows/tests.yml << 'EOF'
name: WordPress Plugin Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        php: [8.0, 8.1, 8.2]
        wordpress: ['6.4', 'latest']
    
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: password
          MYSQL_DATABASE: wordpress_test
        options: --health-cmd="mysqladmin ping" --health-timeout=5s
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: mysql, mbstring, zip
      
      - name: Install Composer dependencies
        run: composer install --no-interaction --prefer-dist
      
      - name: Install WordPress test suite
        run: |
          bash bin/install-wp-tests.sh wordpress_test root password 127.0.0.1 ${{ matrix.wordpress }}
      
      - name: Run PHPUnit tests
        run: ./vendor/bin/phpunit --coverage-text
EOF

# ✅ カバレッジレポートを生成
./vendor/bin/phpunit --coverage-html tests/coverage/
# → tests/coverage/index.html でカバレッジを確認

# ✅ テスト結果の確認
./vendor/bin/phpunit --testdox
# → MyPlugin (MyPluginTest)
# →  ✔ Create post
# →  ✔ Hook is registered
# →  ✔ Save settings

注意事項

  • WP_UnitTestCaseは各テスト後にデータベースのトランザクションをロールバックするため、テスト間でデータが汚染されません。ただし$wpdb->query()を直接使ったDDL文(CREATE TABLE等)はロールバックされません
  • CIでテストを実行する際、bin/install-wp-tests.shはインターネットからWordPressをダウンロードします。キャッシュを活用して実行時間を短縮するために、GitHubActionsのactions/cacheを使ってWordPressのインストールをキャッシュしてください

まとめ

PHPUnitでのWordPressプラグインテストは①wp scaffold plugin-testsでテストスキャフォールド生成・bin/install-wp-tests.shでテスト用DB作成・composer require --dev phpunit/phpunitでPHPUnitインストール、②WP_UnitTestCaseを継承・setUp()/tearDown()でテストの前後処理・$this->factory->post->create()でテストデータ生成、③Brain\Monkeyでget_the_title()等のWordPress関数をモック・Actions\expectDone()でフックの実行を検証、④@dataProviderで複数ケースをまとめてテスト・$this->expectException()で例外テスト・ファクトリーでDBのロールバックを確認、⑤GitHub Actionsのmatrix戦略でPHP/WordPress複数バージョンをテスト・カバレッジレポート生成の手順で実装します。

お気軽にご相談ください

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