最近、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));
}
参考
- Mockery 公式ドキュメント
- PHP 公式ドキュメント
-
Mockery::on()
を使うやり方