LoginSignup
6
3

More than 3 years have passed since last update.

Mockeryでメソッドの引数にオブジェクトを渡したい場合

Posted at

最近、Mockery でモック作ろうとしてハマったので、メモ。

要約

  • Mockery の with() にオブジェクトを指定するとき
    • with($obj) とした場合は厳密比較( === )になってしまう。
    • \Hamcrest\Matchers::equalTo() を使えば緩やかな比較( == )になる。

環境

  • PHP 7.3.7
  • Laravel 5.8.29
    • PHPUnit 7.5.14
    • Mockery 1.2.2

前提

例えば、以下のコントローラをテストする際に、コントローラから呼んでいる ProductService::fetchProduct() をモックにしたいとします。

class ProductController extends Controller
{
    public function fetchProduct(int $paramProductId, ProductService $service)
    {
        $productId = new ProductId($paramProductId);

        // この fetchProduct() をモックにしたい
        $product = $service->fetchProduct($productId);

        return view('product_detail');
    }
}

ProductService::fetchProduct() の第1引数は、 クラス ProductId のインスタンスを受け取る想定です。1

class ProductService
{
    public function fetchProduct(ProductId $productId): Product
    {
        return $this->productRepo->fetch($productId);
    }
}

NG

さて、ここで以下のようなテストコードを書いてみます。
with(new ProductId(1)) で、 ProductId のインスタンスを指定していることに注意してください。

$this->mock(ProductService::class, function($mock) use ($product) {
     $mock->shouldReceive('fetchProduct')
         ->once()
         ->with(new ProductId(1))
         ->andReturn($product);
 })

このテストコードを実行すると、以下のようなエラーがログ出力されます。2

[2019-09-16 02:46:05] local.ERROR: No matching handler found for Mockery_2_App_Application_Services_ProductManage_ProductService::fetchProduct(object(App\Package\ProductManage\Model\ProductId)). 
Either the method was unexpected or its arguments matched no expected argument list for this method

ちゃんとメソッドの引数に ProductId のインスタンスを渡しているし、いったいなにがおかしいんだろう...

ヒント

Mockery の公式ドキュメントには、こう書いてあります。

このようなケースでは、Mockeryはまず引数の比較に===(厳密な比較)演算子を使用します。引数がプリミティブで、厳密な比較で不一致の場合、Mockeryは==(緩やかな比較)演算子をフォールバックとして使用します。

オブジェクトの引数のマッチングでは、Mockeryは厳密な===比較だけを行いますので、全く同じ$objectのみ一致します。

ま た お ま え か

PHP の === でオブジェクトを比較すると、インスタンスの参照先が同じかで比較します。

Mockery で作ったモックで渡した $productId と、コントローラで ProductSerive::fetchProduct() を呼ぶ際に渡した $productId は別々のインスタンスなので、参照先が同じになることはありません。

そして Mockery は === でしか比較してくれないようです。
どうりで、うまく動かないはずです。

OK

ここで万策尽きたかに見えましたが、公式ドキュメントには、続きが書いてありました。

オブジェクトに対してゆるい比較が必要であれば、HamcrestのequalToマッチャーを使用します。

これを踏まえて、 with() で Hamcrest の equalTo() を使うように修正しました。3
Hamcrest は JUnit をやったことがある人には、おなじみのアレですね。4
その PHP へ移植したバージョンが Mockery にはデフォルトで同梱されているようです。

ということで、以下のように書き直してみます。

$this->mock(ProductService::class, function($mock) use ($product) {
     $mock->shouldReceive('fetchProduct')
         ->once()
         ->with(\Hamcrest\Matchers::equalTo(new ProductId(1)))
         ->andReturn($product);
 })

今回はちゃんと動きました!

ちなみに

Hamcrest の equalTo()実装 を見ると、たしかに == で比較していることが分かります。

public function matches($arg)
{
    return (($arg == $this->_item) && ($this->_item == $arg));
}

参考


  1. 本論と関係ないですが、 ProductId はドメイン駆動開発の「値オブジェクト」のつもりで作っています。 

  2. 今回は Laravel を使っているので storage/logs/laravel-yyyy-mm-dd.log に出力されていました。 

  3. 残念ながら公式に書いてある with(equalTo(new stdClass)); のままでは、PHPUnit の equalTo メソッドと区別できず、うまく動かないようです。 

  4. 残念ながら私は案件で JUnit を1回しか触ったことがないので、全然おなじみではない。 

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3