【Mockery】メソッドチェーンのモック、真面目にやるか?グータラやるか?


TL;DR

  • メソッドチェーンのモックは,戻り値を次のオブジェクトのモックにしてあげることで,その一連の処理をモックすることができる
  • -> を用いた省略記法を使えば,最後の戻り値の期待のみで簡単にメソッドチェーンをモックできる
  • ただし,省略記法を使うと途中で渡される引数は全て無視されるので注意が必要

はじめに

Mockery を使い始めて割と最初の方にブチ当たる壁,そう,メソッドチェーンのモック
慣れてしまえばどうってことないんですが,初めはよくわからず,最終的に泣く泣くメソッドチェーンを一時変数で分解する...みたいなこともあるかもしれません.

実は Mockery では,一連のメソッドチェーンをモックする方法が僕が知る限り2つあります.
一つは,チェーンしてるメソッドをそれぞれ厳密にモックする方法.もう一つはかなり省略して簡単にモックする方法です.
どっちを使うかはその場その場で変わってくるとは思いますが,両方知っておくと良いと思います.

本記事では,まず真面目にモックする方法をご紹介し,そのあと同じメソッドチェーンをめちゃめちゃ簡単にモックする方法をご紹介します.

前提

例としてどんなメソッドチェーンをモックするか最初に決めておきます.
なにか気持ちよく沢山チェーンできるものを探したのですが, Illuminate\Auth\AuthManager.php くらいしか思いつきませんでしたので,これを使いましょう.

use Illuminate\Auth\AuthManager;

class Example
{
    public function __construct(
        private AuthManager $auth,
    ) {
    }

    public function example(): void
    {
        $userName = $this->auth
            ->guard()
            ->user()
            ->getAuthIdentifierName();

        echo "ログイン中のユーザーは $userName さんです!";
    }
}

Laravel において,ログイン中のユーザー名を取得するコードです.これを Unit テストすることにします.

真面目パターン

真面目パターンでは,チェーンしているメソッドそれぞれで,戻り値として新たなモックを返し,そのモックで新たにメソッドの呼び出しを期待する...というのを繰り返していきます.
と言ってもイメージが湧きづらいでしょうから,実際のテストコードを示します.

use Illuminate\Auth\AuthManager;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Guard;
use Mockery;
use PHPUnit\Framework\TestCase;

class ExampleTest extends TestCase
{
    public function test_example_正常系(): void
    {
        $auth = Mockery::mock(AuthManager::class);

        // guard() が呼ばれることを期待
        $auth
            ->shouldReceive('guard')
            ->once()
            ->andReturn($guard = Mockery::mock(Guard::class));

        //  チェーンした user() が呼ばれることを期待
        $guard
            ->shouldReceive('user')
            ->once()
            ->andReturn($user = Mockery::mock(Authenticatable::class));

        // チェーンした getAuthIdentifierName() が呼ばれることを期待
        $user
            ->shouldReceive('getAuthIdentifierName')
            ->once()
            ->andReturn('安倍晋三');

        $action = new Example($auth);

        $this->assertSame(
            "ログイン中のユーザーは 安倍晋三 さんです!",
            $action->example(),
        );
    }
}

まず,テスト対象のメソッドにおける $this->auth のモックを作ります.それが $auth = Mockery::mock(AuthManager::class); ですね.
最初に呼ばれるメソッドは guard() なので,shouldReceive('guard') で呼ばれることを期待します.
ここまではまだメソッドチェーンではないので,通常のモックと何ら変わらないと思います.

guard() の実装を見に行くと,

/**
 * Attempt to get the guard from the local cache.
 *
 * @param  string|null  $name
 * @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard
 */
public function guard($name = null)
{
    $name = $name ?: $this->getDefaultDriver();
    return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name);
}

とありますので,このメソッドは Guard::class もしくは StatefulGuard::class のインスタンスを返します.今回は前者を返すものとして,guard() の戻り値として Guard::class のモックを指定します.それが,andReturn($guard = Mockery::mock(Guard::class)); ですね.

今度は,user() が呼ばれますので,$guard->shouldReceive('user')user() が呼ばれることを期待します.

user() のインタフェースを見に行くと

/**
 * Get the currently authenticated user.
 *
 * @return \Illuminate\Contracts\Auth\Authenticatable|null
 */
public function user();

とありますので,Authenticatable::class のインスタンスを返すことを期待します.ここで今回もまた,Authenticatable::class のモックを返してあげるようにします.それが andReturn($user = Mockery::mock(Authenticatable::class)); ですね.

最後に,getAuthIdentifierName() が呼ばれますので,shouldReceive('getAuthIdentifierName')getAuthIdentifierName() が呼ばれることを期待します.

getAuthIdentifierName() のインタフェースは見に行くまでもありませんが,

/**
 * Get the name of the unique identifier for the user.
 *
 * @return string
 */
public function getAuthIdentifierName();

とありますので,適当に名前を返してあげるようにすればよいです.

これで,一連のメソッドチェーンのモックが完了です!

この方法は,一つ一つモックを作ってはメソッド呼び出しを期待するというのを繰り返すのでかなりめんどくさいですが,引数の検査などもそれぞれ行えるため厳密なテストをすることができます.基本は,この方法を使うのが良いでしょう.

サボりパターン

めんどくさがりやのあなたへ.Mockery には デメテルチェーンとfluentインターフェイスのモック というのがあります.