前回は、Mockeryを使用したテストの基本的な書き方について解説しました。今回はモックしにくいケースでどのようにテストを書くのか、overloadを使った方法をご紹介します。

前回はこちらから閲覧できます。

モック対象を new しているケース

テスト対象クラスは以下のようになっています。外部APIを介してデータを取得しリストを返却、という部分は前回と同じですが、constructの部分は少し違います。

class TestApi
{
  private $connection;

  public function __construct()
  {
    $this->connection = new TestOAuth;
  }

  public function getList(string $param)
  {
    $list = $this->connection->get($param);

    return $list->statuses;
  }
}

モック対象であるTestOAuthを、インジェクションではなくconstructの中でnewしています。これではTestOAuthのモックを作成しても、前回のように引数として渡すことができません。

overloadを付けてモックを作成

そこで使用されるのがoverloadです。

モック作成時、以下のようにoverloadプレフィックスを付けることでプロダクトコードを変更することなく対象をモックできます。

        \Mockery::mock('overload:' . TestOAuth::class);

テストコード全体は以下のようになります。モックオブジェクト作成時にoverloadを使用している以外は、ほぼ前回と同じように記述しています。

    public function getListTest()
    {
                //モックを作成
        $mock = \Mockery::mock('overload:' . TestOAuth::class);

        $mock->shouldReceive('get')
            ->once()
            ->with('test')
            ->andReturn((object) [
                'statuses' =>
                [
                    (object)['mockからのレスポンス1'],
                    (object)['mockからのレスポンス2']
                ]
            ]);

        //テストここから
        $testApi = new TestApi();

        $actual = $testApi->getList('test');

        $this->assertCount(2, $actual);
    }

onceでメソッドが1度だけ呼ばれることを、withでメソッドに渡される引数が「test」であることを、期待値としてモックオブジェクトに設定しています。

またandReturnで、APIからのレスポンスが2つのオブジェクトを含む配列を持つstatusesプロパティが返るようにしました。

これでテストを実行すると、

% vendor/bin/phpunit --filter getListTest          
PHPUnit 9.5.16 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 00:00.420, Memory: 22.00 MB

問題なく通りました。

ちなみに、overloadするだけで本当にモックが使用されているのか?と心配になるかと思いますので、モックに差し代わっているかどうかチェックしてみます。

プロダクトコード内、モックからのレスポンスを受け取っている変数をダンプして中身を確認します。以下の箇所ですね。

    $list = $this->connection->get($param);

テストを動かしながら$listの中身を確認すると、以下のようになりました。

 {#727
  "statuses": array:2 [
    0 => {#328
      "0": "mockからのレスポンス1"
    }
    1 => {#333
      "0": "mockからのレスポンス2"
    }
  ]
}

ちゃんとモックオブジェクトに設定した値が返ってきています。

実際の呼び出しがモックの期待値と異なるケース

モックオブジェクトに設定した期待値と、実際の結果が異なった場合どのような挙動になるのでしょうか。エラーケースを見てみましょう。

まずは、呼び出し回数が異なる場合です。今回のテストではgetメソッドが1回呼ばれることを期待していました。

しかし実際にはget2回呼ばれたとします。テストを実行すると、以下のエラーとなりました。

    Mockery\Exception\InvalidCountException: Method get(<Any Arguments>) from Abraham\TestOAuth should be called
    exactly 1 times but called 2 times.     

エラーメッセージにも、2度呼ばれた旨が書いてあって分かりやすいですね。

次は、引数が期待と異なった場合です。引数が「test」であることを期待していたところ、実際に使用された引数は「テスト」だったとします。

テストを実行すると・・・

   Mockery\Exception\NoMatchingExpectationException: No matching handler found for Abraham\TestOAuth::get('テスト'). Either the method was unexpected or its arguments matched no expected argument list for this method  

こちらも無事エラーとなり、エラーメッセージには引数がマッチしない旨の記述があります。

overloadは対象クラスがすでにnewされている場合には使えないなど、少し制約はありますがモックが難しい場面ではとても頼りになるのでぜひ活用してみてくださいね。

By hmatsu