AspectMockでstaticメソッドのパーシャルモック作成と注意点


導入

PHPのAspectMockについて利用してみた。
そこで気づいた注意点と簡単な利用方法について記載していく。

動機

PHPのstaticメソッドを保有するクラスで、
staticメソッドの中身で自分自身のstaticメソッドを呼んでいるパターンがあったとする。
(これがいいのかという話はおいておいて...)
これはよくある、PHPUnit+Mockitだとパーシャルモックを利用したテストができない...。

例えば以下のような形

TestTarget.php
<?php

namespace App;

class TestTarget
{
    /**
     * 与えられた整数を2倍にして返す
     *
     * @access public
     * @param int $num
     * @return int
     */
    public static function doubling(int $num): int
    {
        return $num * 2;
    }

    /**
     * 与えられた整数の配列を2倍にして返す
     *
     * @access public
     * @param array $numArray
     * @return array
     */
    public static function doublingArray(array $numArray): array
    {
        $newArray = [];
        foreach ($numArray as $num) {
            $newArray[] = self::doubling($num);
        }
        return $newArray;
    }
}

そこで一つの解決策としてAspectMockを利用してみた。

AspectMockとは

PHPでアスペクト指向プログラミングを実現するための、GoAOPというフレームワーク
https://github.com/goaop/framework

これを利用して、柔軟にモックを作成できるライブラリがAspectMockである。
https://github.com/Codeception/AspectMock

これを使えば、クラス全体をモック化するような操作ではなく、
クラスを部分的に書き換えることができる。

環境

  • PHP7.1
  • Laravel5.5
  • PHPUnit6.5

インストール方法

以下の2つのパッケージをインストールする
- goaop/framework
- codeception/aspect-mock

$ composer require --dev goaop/framework codeception/aspect-mock

(余談)
今回は依存関係の問題でエラーが出たので、特定パッケージを解決できるバージョンに固定
goaop/parser-reflection ~2.2nikic/php-parser ~3.0の依存関係があったので、
バージョンを調整後に、必要なパッケージをインストールした

$ composer require nikic/php-parser ~3.0

設定

autoloadへの設定

composerのオートローダーを使っている場合、
テスト実行時にデフォルトでvendor/autoload.phpが読み込まれている

$ cat phpunit.xml | grep autoload
         bootstrap="vendor/autoload.php"

vendor配下のファイルは書き換えるべきではないので、
テスト用の設定を記述するために
bootstrap/autoload_test.phpというファイルを作成して

bootstrap/autoload_test.php
<?php
include __DIR__.'/../vendor/autoload.php'; // composer autoload

$kernel = \AspectMock\Kernel::getInstance();
$kernel->init([
    'debug' => true,
    /** 
     * 利用するソースコードのパスを記述する
     * できればまとめずに利用するファイルを一つずつ記述していったほうがよい(理由は後述)
     */
    'includePaths' => [
        __DIR__.'/../app/TestTarget.php',
    ],
    /**
     * AspectMockのキャッシュファイル置き場を設定
     */
    'cacheDir' => __DIR__.'/../storage/aspectmock/'
]);

PHPUnitのbootstrapに設定

$ cat phpunit.xml | grep autoload
         bootstrap="bootstrap/autoload_test.php"

.gitignoreにキャッシュディレクトリを追加

$ echo 'storage/aspectmock' >> .gitignore

注意点

仕組みとして、includePathに設定した該当のPHPファイルに対して、
各メソッドの先頭に特有の処理が書かれたコピーをcacheDirに設定されたディレクトリに作成され、
そちらのファイルが実行されることにより、アスペクト指向を利用した柔軟なモック作成を実現する。

例)


    /**
     * 与えられた整数を2倍にして返す
     *
     * @access public
     * @param int $num
     * @return int
     */
    public static function doubling(int $num): int
    { if (($__am_res = __amock_before(get_called_class(), __CLASS__, __FUNCTION__, array($num), true)) !== __AM_CONTINUE__) return $__am_res;
        return $num * 2;
    }

つまり、includePathに設定したディレクトリ配下に、対象ファイルがたくさん存在すると、
特に初回実行時にテストの実行に時間がかかってしまう。
テストを実行するCI環境がDocker等の破棄される前提の環境である場合、CIの実行の遅延の原因となりうる。
そのため、AspectMockを利用する部分をピンポイントにincludePathに設定するほうが良さそう。

利用例

以下の2つのテストだけ実行する、2つ目はAspectMockの機能を利用する。
- doublingメソッドが引数で受け取った数値を2倍にして返す正常処理
- doublingメソッドだけをテストダブルで書き換えて、それに依存しているdoublingArrayを実行してみる

tests/Unit/TestTargetTest.php
<?php

namespace Tests\Unit;

use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\TestTarget;
use AspectMock\Test as AspectMockTest;

class TestTargetTest extends TestCase
{
    /**
     * @test
     */
    public function doublingメソッドが2倍で正常に返してくれる()
    {
        $actual = TestTarget::doubling(3);
        $expected = 6;
        $this->assertSame($expected, $actual);
    }

    /**
     * @test
     */
    public function doublingをメソッドを3倍に書き換えて失敗してみる()
    {
        // テストダブルの作成
        AspectMockTest::double(TestTarget::class, [
            'doubling' => function($num) {
                return $num * 3;
            },
        ]);
        $actual = TestTarget::doublingArray([1,2,3]);
        $expected = [2,4,6];
        $this->assertSame($expected, $actual);
        AspectMockTest::clean(); // テストダブルの削除
    }
}

テストを実行してみると、テストダブルを生成しているテストだけ、
意図通りにモック化されたメソッドが使用されていることがわかる。


$ ./vendor/bin/phpunit tests/Unit/TestTargetTest.php

.F                                                                  2 / 2 (100%)

Time: 129 ms, Memory: 14.00MB

There was 1 failure:

1) Tests\Unit\TestTargetTest::doublingをメソッドを3倍に書き換えて失敗してみる
Failed asserting that Array &0 (
    0 => 3
    1 => 6
    2 => 9
) is identical to Array &0 (
    0 => 2
    1 => 4
    2 => 6
).

FAILURES!
Tests: 2, Assertions: 2, Failures: 1.

最後に

今回は簡単な例でしたが、例外の処理だったり特定条件下でオリジナルのメソッドを呼び出すようにしたりと、
柔軟にテストがしやすくなるような機能が搭載されており、その記述もかなり簡単なので非常に便利なライブラリです。
一方で、テスト実行時間などのパフォーマンス面でやや不安を覚えるところがあるので、
大規模なシステムの場合は扱い方に気をつけたほうが良さそうです。