PHP テストの作成

WordPress の PHPUnit テストスイートについて

WordPress の PHPUnit テストスイートには、何千もの自動テストが含まれています。自動テストは、WP の機能の特定の部分を検証する小さなコードです。これらのテストは、機能開発やリグレッションを防止するための強力なツールです。

自動テストは、できるだけ小さく、具体的なものにする必要があります。理想的には、自動テストはユニットテストであるべきです。システム全体の状態に依存することなく、機能の一部を完全に分離して検証するテストです。実際には、WordPress とそのテストスイートの構造上、「純粋な」ユニットテストを書くことは難しいか、不可能です。ここでも他の場所でも、スイート内の自動テストを大まかに指すために「ユニットテスト」という用語を使用します。

テストの基本構造

アサーション

あらゆるテストで最も重要なものはアサーションです。アサーションとは、システムから得られる期待値と、実際に得られた値との比較です。最も単純なテストは、単一のアサーションだけで構成されるかもしれません。例:

public function test_trailingslashit_should_add_slash_when_none_is_present() {
    $this->assertSame( 'foo/', trailingslashit( 'foo' ) );
}

assertSame() メソッドは、期待値 (この場合、ハードコードされた文字列 'foo/') と 実際の値 (trailingslashit() によって返される値) の2つのパラメータを受け入れます。

一般的なアサーションのアノテーション付きリストは以下にあります。

フィクスチャとファクトリ

多くのテストでは、投稿やユーザーなど、WordPress が通常データベースに保存している1つ以上のデータオブジェクトの存在が必要です。個々のテストメソッドは空の WordPress インストールから始まるため、各テストはテスト用の独自のオブジェクトを作成する必要があります。これらのオブジェクトはフィクスチャと呼ばれ、ファクトリによって生成されます。

public function test_user_with_editor_role_can_edit_others_posts() {
    $user_id = self::factory()->user->create( array(
        'role' => 'editor',
    ) );

    $this->assertTrue( user_can( $user_id, 'edit_others_posts' ) );
}

create() ファクトリメソッドはオブジェクトの数値 ID を返します。代わりにオブジェクトを取得するには、create_and_get() を使用します。

public function test_user_exists() {
    $user = self::factory()->user->create_and_get();

    $this->assertTrue( $user->exists() );
}

テストによっては、同じ種類のオブジェクトを複数必要とするものがあります。この場合は create_many() を使用します。これはイテレータを使用してオブジェクトの一意性を保証します (たとえば、ユーザーの場合は一意のメールアドレス):

public function test_term_query_count() {
    $tags = self::factory()->term->create_many( 3, array(
        'taxonomy' => 'post_tag',
    ) );

    $term_query = new WP_Term_Query();
    $actual = $term_query->query( array(
        'taxonomy' => 'post_tag',
        'fields' => 'count',
    ) );

    $this->assertSame( 3, $actual );
}

ファクトリの create*() メソッドは、そのオブジェクトタイプに固有の引数の配列を受け取ります。詳しくは、https://core.trac.wordpress.org/browser/trunk/tests/phpunit/includes/factory で各ファクトリクラスの __construct() メソッドを参照してください。

共通のフィクスチャ

テストクラス内のすべてのテストが同じフィクスチャを利用できるようにする必要がある場合、これらのフィクスチャの作成を「set up」メソッドに抽象化することが一般的です。また、テスト後にクリーンアップし、作成したフィクスチャを削除することも良い習慣です。

PHPUnit は4つのメソッドを提供しており、これらは自動的にコールされます: setUpBeforeClass()setUp()tearDown()tearDownAfterClass() です。

WordPress のテストスイートでは、これらのメソッドの snake_case バージョンを必ず使わなければなりません。これらの snake_case メソッドは、PHPUnit のバージョン間の互換性を提供します。

PHPUnit のメソッド名WordPress のメソッド名
setUpBeforeClass()set_up_before_class()
setUp()set_up()
assertPreConditions()assert_pre_conditions()
assertPostConditions()assert_post_conditions()
tearDown()tear_down()
tearDownAfterClass()tear_down_after_class()

set_up() メソッド内の関数はクラスの各テストの前に実行され、tear_down() メソッド内の関数はクラスの各テストの直後に実行されます。対照的に、set_up_before_class() メソッドはクラスがインスタンス化される前に一度だけ実行され、tear_down_after_class() メソッドはクラス内のすべてのテストが実行された後に一度だけ実行されます。

デフォルトでは、WordPress のテストスイート内のすべてのテストクラスは WP_UnitTestCase クラスを継承しており、このクラスは WP_UnitTestCase_Base クラスを継承しています。

WP_UnitTestCase_Base クラスには、これらのフィクスチャメソッドの一般的な宣言がすでに含まれており、データベースをデフォルトの状態にリセットしたり、投稿タイプやタクソノミー、いくつかのグローバル変数、登録済みのフックなどをクリーンアップしたりします。

テストクラスで set_up() メソッドを宣言する場合、set_up() メソッドの最初の行が parent::set_up() の呼び出しであることが重要です。同様に、tear_down() メソッドは常に parent::tear_down() を最後の行として呼び出す必要があります。

class Tests_Subset_functionName {
    public function set_up() {
        parent::set_up();
        // The specific set up for the tests within this class.
    }

    public function tear_down() {
        // Clean up specific to the tests within this class.
        parent::tear_down();
    }
}

どのメソッドをいつ使用するかについての詳細は、PHPUnit マニュアルのフィクスチャの章を参照してください。

アサーションの使用

PHPUnit で使用できるもっとも基本的なアサーションは assertSame() で、これは $expected パラメータと $actual パラメータが === であるかどうかを厳密にチェックします。ほぼすべてのテストは、このアサーションを使用して作成できます。

便利なことに、PHPUnit ではさらに多くのアサーションメソッドを使用できます。利用可能な最も具体的なアサーションを使用することを強くおすすめします。公式ドキュメントを参照してください。WP 5.9以降では、PHPUnit 9.x で使用できるすべてのアサーションが WordPress のテストスイートで使用でき、PHPUnit のクロスバージョンで動作します。

よく使われるアサーションメソッドのいくつかを以下に示します:

  • assertContains()assertNotContains()
  • assertStringContainsString()assertStringNotContainsString()
  • assertTrue()assertFalse()
  • assertNull()assertEmpty()assertNotEmpty()

PHPUnit が提供するアサーションメソッドに加えて、WordPress には独自のアサーションメソッドが多数用意されています:

  • assertEqualSets( $expected, $actual, $message = '' ) – 順序や厳密な等式を無視して、2つの配列の等価性を保証する (例 assertEqualSets( [ 1, 2 ], [ '2', '1' ] ))
  • assertEqualSetsWithIndex( $expected, $actual, $message = '' )
  • assertSameSets( $expected, $actual, $message = '' )
  • assertSameSetsWithIndex( $expected, $actual, $message = '' )
  • assertWPError( $actual, $message = '' )assertNotWPError( $actual, $message = '' )
  • assertIXRError( $actual, $message = '' )assertNotIXRError( $actual, $message = '' )
  • assertEqualFields( $object, $fields, $message = '' )
  • assertDiscardWhitespace( $expected, $actual, $message = '' )
  • assertSameIgnoreEOL( $expected, $actual, $message = '' )
  • assertNonEmptyMultidimensionalArray( $array, $message = '' )

完全なリストは、WP_UnitTestCase_Base クラスを参照してください。

PHPUnit のすべてのアサーションと WordPress のすべてのカスタムアサーションでは、$message パラメータを渡すことができます。このメッセージはアサーションが失敗したときに表示され、テストのデバッグにとても役立ちます。テストメソッド内で複数のアサーションを使用する場合は、このパラメータを必ず使用する必要があります。

class Tests_Foo {
    public function test_function_call() {
        $actual = function_call();

        $this->assertIsArray( $actual, 'Return value of function_call() is not an array' );
        $this->assertArrayHasKey( 'desired_key', $actual, 'Key "desired_key" does not exist in array returned by function_call()' );
        $this->assertSame( 'expected_value', $actual['desired_key'], 'Value for array key "desired_key" does not match expectations' );
    }
}

アノテーション

PHPUnit アノテーションとは、メソッドやクラスの docblock に記述するメタデータのことで、@ で示されます。WordPress のテストでは、テストを整理するために、多数の PHPUnit アノテーションといくつかのカスタムアノテーションが使用されます。クラスに属するアノテーションはすべてのメンバーのテストメソッドによって自動的に継承されるため、ピリオドを追加しないでください。

よく使われるアノテーションのいくつかを以下に示します:

  • @group – テストやテストクラスを機能ごとに分類してグループ化し、個別に実行できるようにします (例: $ phpunit --group comment)。すべてのテストクラスは少なくともひとつの @group アノテーションを持つべきで、個々のテストは必要に応じてさらに @group アノテーションを持つべきです。
  • @covers – 特定のテストで実際にテストされる関数、メソッド、またはクラスにアノテーションを付けるために使用されます。すべてのテストに @covers タグを追加することを強くおすすめします
  • @requires – このアノテーションは、テストが特定の PHP (最小) バージョン、PHP 拡張、または特定の関数に依存していることを示すために使用します。そのような依存関係が存在し、それが満たされている場合にのみテストが成功する場合は、@requires タグをテストの docblock に追加する必要があります。PHPUnit マニュアルに、このタグに関する優れた例がいくつかあります。
  • @ticket – WordPress のカスタムアノテーション。チケット #12345 で説明されているバグに対応したテストであることを示すには、@ticket 12345 を使用します。内部的には、@ticket アノテーションは @group に変換されるため、テストの実行を特定のチケットに関連するものに限定できます: phpunit --group 12345
  • @expectedDeprecated – WordPress 独自のものです。指定した関数/メソッド/クラスが _deprecated_*() 通知をスローすることが期待されていることを示します。このアノテーションがないと、非推奨通知が発生するテストは失敗します。同様に、このアノテーションを含めてもテストで非推奨通知が発生しない場合は、テストは失敗します。たとえば、非推奨の like_escape() に対するテストには @expectedDeprecated like_escape というアノテーションが含まれています。
  • @expectedIncorrectUsage@expectedDeprecated と似ていますが、_doing_it_wrong() の通知のためのものです。

名前と構成

WordPress チームは、テストを見つけやすく、保守しやすく、単体で実行しやすくするよう努めています。そのため、テストを整理したり名前をつけたりするためのいくつかのガイドラインを用意しています。

すべての PHPUnit テストは /tests/phpunit/tests/ ディレクトリにあります。以下に示すパスは、このルートからの相対パスとなります。

テストクラス

テストはクラスにまとまっています。同じ機能の異なる部分をテストする場合は、テストを一つのクラスにまとめます。特に、共通の set_up()set_up_before_class() ルーチンを使用する場合は、テストをひとつのクラスにまとめると便利です。原則として、ひとつのテストクラスに複数の関数やメソッドのテストを含めるべきではありません。

各テストクラスは、それ自身がファイルでなければなりません。テストファイルは共有された機能にもとづいてフォルダーに分類されます。共有機能は通常、それらのテストのプライマリ @group アノテーションと一致します。テストファイルにはキャメルケースを使用して名前を付ける必要があります。

テストクラス名はファイルパスを反映したものにし、アンダースコアをディレクトリの区切り文字に、タイトルケースをキャメルケースに置き換えてください。したがって、get_comment_class() 関数のテストを含む Tests_Comment_GetCommentClass() クラスは tests/comment/getCommentClass.php にあります。

テストメソッド

テストクラスのメソッド名は、必ず test_ で始めなければなりません。test_ で始まらないメソッドは、PHPUnit によって実行されません。フィルターコールバックまたは他のユーティリティメソッドには、この接頭辞を付けることはできません。テストメソッド名はスネークケースで記述する必要があります。

テスト名は、できるだけわかりやすいものにしてください。たとえば Tests_Comment_GetCommentClass::test_should_accept_comment_id() は、get_comment_class() がコメント ID を受け入れるかどうかをテストします。テストの望ましい結果を表すテスト名 (理想的には「should」という単語を含む) は、デバッグがはるかに簡単になります。適切なテスト名であれば、テストの実行中に失敗が見つかったときにテストのソースを見る必要がなくなるかもしれません。

高度なトピック

遅いフィクスチャ

場合によっては、クラス内のすべてのテストで同じデータベースフィクスチャを使うことがあります。これらのフィクスチャを set_up() の中で生成すると、テストごとにフィクスチャが再作成されることになり、テストのパフォーマンスが大幅に低下する可能性があります。このような場合は wpSetUpBeforeClass() を使用して、クラスのテストが実行される前にフィクスチャを一度だけ作成します:

class Tests_Comment_Stuff {
    protected static $comment_post_id;

    public static function wpSetUpBeforeClass( $factory ) {
        self::$comment_post_id = $factory->post->create();
    }
}

同様に、wpTearDownAfterClass() はクラスのテストがすべて実行された後のクリーンアップのために使用できます (ただし、テストスイートはすべての共有フィクスチャを自動的にクリーンアップしようとするので、一般的には不要であることに注意してください)。

共有フィクスチャに関するいくつかの注意点:

  • wpSetUpBeforeClass()wpTearDownAfterClass() は static として宣言し、静的に呼び出す必要があります。
  • メソッドは静的であるため、テストメソッドで使用するためにデータを保存する方法も静的である必要があります。つまり、$this->foo ではなく self::$foo です。
  • イテレータの一意性が壊れてしまうため、wpSetUpBeforeClass() 内で self::factory() を呼び出さないでください。メソッドに渡される $factory オブジェクトを常に使用してください。

反復テスト

テストメソッドのコードをコピー & ペーストして、同じ WordPress の機能を少し異なる引数でテストしたくなったときは、代わりにデータプロバイダの使用を検討してください。

データプロバイダは、テストメソッドの docblock にある @dataProvider アノテーションを使ってテストにリンクされる二次関数のことで、一般的にはメソッド名の test 接頭辞を data に置き換えてテスト名にします。

この2番目の関数は、二次元配列を返す必要があります。この配列内の各エントリーは「データセット」と呼ばれ、入力パラメータとしてテスト関数に渡されます。

データプロバイダでは、名前付きデータセットを使用することを強くおすすめします。

フックのための一度限りの関数

アクションやフィルターにフックするための一度限りのコールバック関数が必要な場合は、クロージャ (無名関数) を自由に使ってください。

また、クロージャが $this 変数を使用しない場合は、最適なパフォーマンスを得るためにクロージャを static として宣言してください。

コア、プラグイン、テーマのフックのコールバックとしてクロージャを使用することは推奨されません。無名関数では、フックからコールバックを外すことができないからです。しかし、これはテストスイートでは問題ありません。登録されたアクションとフィルターのスタック全体は、共有された tear_down() メソッドによってテストのたびにリセットされるからです。

技術概要

興味のある方のために、WordPress のようなアプリケーションで PHPUnit がどのように使われるのかを簡単に説明します。

WordPress インストールと ブートストラップ

phpunit が起動されると、GUI インストールと同様の構成で、テストスイートは WordPress のデフォルトインストールを設定するスクリプトを実行します。テストが実行される前に、次の手順が行われます:

  • (wp-settings.php をインクルードすることで) WordPress を起動します。これは、すべてのテストが (wp_loaded にを通じて) WP ブートストラップ全体の後に実行されることを意味します。
  • デフォルトのコンテンツはすべて削除されます。これにはサンプルの投稿とページが含まれますが、デフォルトのユーザーや「未分類」カテゴリーは含まれません

グローバル

$wp$wp_query など、現在のページの状態を反映する WP グローバルは、テストを実行するたびにデフォルトの状態にリセットされます。同様に、$post$more などの投稿関連のグローバルも、テストが実行されるたびに null に設定されます。テストスイートは、ランタイムに登録されたオブジェクトタイプもデフォルトにリセットします。これは、カスタム投稿タイプやタクソノミーなどをテストごとに再登録する必要があることを意味します。最後に重要なことですが、すべてのアクションとフィルターも各テストの後に元の状態にリセットされるので、テストで追加されたフックを手動で削除する必要はありません。

他のグローバルへの変更は、テスト自体で慎重に管理する必要があります。そのための簡単な規約は、元のグローバルをローカル変数に格納し、テストロジックの実行後にその値にリセットすることです。このようなリセットは、実際のアサーションが行われる前に行うことが重要です、そうすることで、一つの失敗によってその後のテスト結果が汚染されることがなくなります。

データベース

WordPress は、データ (投稿、ユーザー) と状態 (オプション) の両方をデータベースに保持します。そして WordPress の構造は、実際にデータベースを使わずにフィクスチャや設定をモックすることはほとんど不可能です。そのため、テストスイートは WP アプリケーションのセットアップとフィクスチャやその他のデータの保存に MySQL データベースを使用します

テストスイートでは、永続的なデータベースと永続的でないデータベースのコンテンツを区別することが重要です。phpunit が起動されると、テストスイートはテストデータベースを消去してクリーンインストールを行います。このデータ (wp_options のデフォルトの内容など) は、テスト中も持続します。

一方、テスト中に行われたデータベースの変更は永続的ではありません。各テストの前に、スイートは autocommit を無効にして MySQL のトランザクション (START TRANSACTION) を開き、各テストの終了時にトランザクションをロールバック (ROLLBACK) します。つまり、テストフィクスチャの作成など、テスト内から実行されたデータベース操作は各テストが終了した後に破棄されます。トランザクションの詳細については、MySQL 公式ドキュメントを参照し、特にコミットをトリガーするステートメントのセクションを参照してください。

原文 / 日本語訳

s
検索
c
新規投稿を作成する
r
返信
e
編集
t
ページのトップへ
j
次の投稿やコメントに移動
k
前の投稿やコメントに移動
o
コメントの表示を切替
esc
投稿やコメントの編集をキャンセル