WordPressでPHPUnitを使って単体テストを行う

社内教育文書公開シリーズです。今回はPHPUnitと単体テストの導入方法をまとめました。

単体テストとは?

そもそも単体テストとはなんぞや、という話ですが、「この関数は期待通りに動くのか?」をテストするものです。基本的にはメソッド・関数単位で行います。

この単体テストをGitHub ActionsなどのCI/CDに組み込むことで、「テストが通っている限りはその関数は壊れていない」と保証されます。もちろん、テストが正しいことが前提ですが。

PHPではPHPUnitが定番かつほぼ一択。たとえば、郵便番号を受け取ったら 123-4567 のようなフォーマットで必ず返してくれる my_zip という関数を作ったとします。これが本当にいつも必ず動くのかをテストしたい場合は次のようなテストコードを書きます。

class Basic_Test extends WP_UnitTestCase {
    public function test_zip() {
        // ハイフンなしもなおるか?
        $this->asertEquals( my_zip('1234567'), '123-4567' );
        // 全角数字も受け入れる?
        $this->asertEquals( my_zip('123ー4567'), '123-4567' );
        // 桁が多い場合は仕様通りWP_Errorを返す?
        $this->assertWPError( my_zip('1234567890') );
        // .....
    }
}

これがユニットテストです。全部が成功するとこんな感じでオッケーマークが出ます。

PHPUnit 9.6.24 by Sebastian Bergmann and contributors.
.............................................                     45 / 45 (100%)

Time: 00:00.883, Memory: 58.50 MB
OK (45 tests, 156 assertions)

GitHub Actionsなどに組み込めば、「エラーが出ていたらプルリクエストをマージしない」「デプロイしない」という安全策を取れますね。

WordPressではどうやって実行しているのか

WordPressのコアは自動テストを導入しており、一連のテストスイートの中にPHPUnitが含まれています。現在はWordPressコアのリポジトリにコミットするとGitHub Actionsで一連のテストが走ります。

 

WordPressのGitHubにプルリクエストを送ると走るテスト

上記は私が以前送ったプルリクエストなのですが、全部コケていますね。これを直さないとマージしてもらえません……。

WordPressのユニットテストは「WordPressを動かしてその中でテストを実行する」という状態なので、「これは単体テストではなく結合テストでは?」とよく疑問に思うのですが、ユニットテストと呼ばれます。たぶんPHPUnitを使っているからUnittestと呼ばれているだけの気もしますね。同じくPHPUnitを使うLaravelだとFeature/Unit Test と呼び名を分けています。

いま調べたら日本語ドキュメントの自動テストに関する項目がめちゃくちゃ充実していたので、WordPress自体のテストがどのように行われているかは目を通しておいてもらうと良いかもしれません。

プラグインやテーマにどう組み込むか

上述したテストスイートはプラグインやテーマでも利用することができます。

  • WP-CLIのscaffoldコマンドを使う。必要なファイルが全部揃う。
  • 上記でインストールされる install-wp-tests.sh とリポジトリに入れる(手動管理)
  • wp-envを使う。Dockerが二個立ち上がるのだが、そのうち一個にはテストスイートが最初からインストールされている。

個人的におすすめはwp-envを使うことです。Dockerを使わない場合、MySQLをローカルに用意する必要があるのですが、これがめんどくさいです。私は長年ローカルのMacに開発用のMySQLを入れていましたが、ずっと邪魔だな……と感じていました。

で、composerを使って以下のものをインストールします。

  • PHPUnit: たしかPHPUnit 10以上には対応していないはず。PHPUnit Compatibility and WordPress Versions を参照のこと。
  • yoast/phpunit-polyfills: 複数のPHPバージョンでPHPUnitを試したい時に必須。これを入れるとPHPUnitも対応したバージョンが自動ではいるはず? PHPUnit 11以上(なぜか10は除外)もサポートしています。
composer require --dev phpunit/phpunit

また、テストのbootstrapファイルも必要です。これはテストスイート上でWordPressを動かし、テーマなりプラグインなりを読み込む処理が書かれています。

と、ここから先は長くなりそうなので、例によって学習用のリポジトリ tarosky/my-first-unittest-on-wp を用意しました。あとはそちらをご覧ください。学習できる内容は以下の通りです。

  • 基本的な関数、クラスメソッドのテスト
  • WordPressの機能と統合したテスト

AjaxやREST APIのテストはまだ追加していないので、将来的に追加するかもしれません。

どれだけテストを書くか

さて、リポジトリを見ると「テストコードが多いな」ということを感じるかもしれません。安全に倒す場合、テストコードはどんどん増えていきます。が、時間は有限なので、まずは1個でもいいからテストを書くことから始めましょう。

私はこれまで何十個もプラグインを公開してきたのですが、CI/CDにテストが組み込まれていると、少なくとも「構文エラーがあってプラグインを更新したら画面が真っ白になった」という事態を防ぐことができます。それだけでもテストを導入する意味があります。

その上でテストの優先順位を決めていくことが必要です。これは以前紹介したe2eテストでも同じです。

  • 書きやすい部分(価格計算やフォーマット関数)にテストコードを追加する
  • そのプラグイン、テーマにとって核心的な部分をカバーしていく

上記の原則にしたがって徐々にテストカバレッジを上げていきましょう。また、以下のような部分はあまりテストしなくても大丈夫です。

  • ただのラッパー関数
  • WordPressのコアが保証している部分(WP_Queryを頑張ってテストしない)

ここら辺の切り分けは経験がものをいうので、コードレビューなどで指摘を受けながら学んでいくとよいでしょう。

テスタブルなコードを書く

一番大事なのはそもそもテスト可能なコードを書く、ということです。たとえば、投稿のときに何かをするフィルターフックを追加したとしましょう。次のようなパターンはよくあるのですが……

add_action('save_post', function( $post_id ) {
    // ここでダラダラ処理を書く
    update_post_meta( $post_id, 'some_key', 'some_var' );
} );

これだけだとテストが大変です。実際に投稿を挿入して試すという方法もありますが、次のように書けばライトに確認できます。

add_action( 'save_post', function( $post_id ) {
    // ここら辺で色々権限チェックを行う。投稿タイプのチェックとか。
    if ( ! wp_verify_nonce( filter_input( INPUT_POST, '_mymodificationnonce' ), 'update_modification' ) ) {
        return;
    }
    // 投稿IDを受け取って何らかの処理を加える関数を作っておく
    my_modification_func( $post_id );
} );

// テスト対象にしやすい
class Mofidy_Test extends WP_UnitTestCase {
    public function test_modification() {
        // 投稿を作成
        $post_id = self::factory()->post->create( [
            'post_tile'    => 'Hello World',
            'post_status'  => 'publish',
            'post_content' => 'Lorem ipsum dolor',
        ] );
        my_modification_func( $post_id );
        // 結果を確認
        $this->assertEquals( 'some_var', get_post_meta( $post_id, 'some_key', true ) );
    }
}

「privateメソッドはテストしづらいからprotectedにする」とか、色々細かいやり方はあるのですが、とにかく「この処理はテスト可能か?」を心がけましょう。

以上でWordPressにおけるPHPUnitの採用方法の紹介は終わりです。テストを導入すれば安心してデプロイできますね。