1円でも得したいWebエンジニアの日常

クーポンだったりクレジットカードのポイントだったりを利用して1円でも得しつつ生活を便利にしていきたいWebエンジニアによるブログ。技術的な記事から商品レビューなど日常的なことまで。

【PHP】Mockeryでクラスとインタフェースを継承したクラスをモックしたり、複数のインタフェースを実装したクラスをモックする

f:id:yoshiki_utakata:20190111231914p:plain

はじめに

PHPのGuzzleHttp *1 を利用し、以下のようなコードを書いた。

<?php

try {
    // $this->guzzleClient は コンストラクタDIなどを利用して
    // $this->guzzleClinet = new \GuzzleHttp\Client(); を差し込むようにする
    $response = $this->guzzleClient->post($url);
} catch (\GuzzleHttp\Exception\GuzzleException $e) {
    // GuzzleExceptionを独自定義の例外に置き換えて投げる
    throw new MyInternalErrorException();
}

ここで、 GuzzleException をキャッチしたあとの処理が正しく行われているかをテストしたい。

環境

問題点

通常であれば以下のようにテストをしたい

<?php

public function test() {
    $this->expectException(MyInternalErrorException::class);
    $this->guzzleClient->shouldReceive('post')
        ->andThrow(new \GuzzleHttp\Exception\GuzzleException());
    // 実際にメソッドが呼んで例外が投げられるかを確認する
}

しかしここで問題がある。 GuzzleException は interface なので *2 new できないのだ。

<?php

namespace GuzzleHttp\Exception;

interface GuzzleException {}

次に以下を試す

<?php

public function test() {
    $this->expectException(MyInternalErrorException::class);
    $this->guzzleClient->shouldReceive('post')
        ->andThrow(\Mockery::mock(\GuzzleHttp\Exception\GuzzleException::class));
    // 実際にメソッドが呼んで例外が投げられるかを確認する
}

これもまだ問題がある。GuzzleException はただの interface であり、Throwable を実装していないので、例外として投げることはできず Mockery でエラーとなる。

仕方ないので、実際に GuzzleException を継承したクラスをThrowすることが考えられる。例えば、Guzzleには TransferException というものがある。

<?php

namespace GuzzleHttp\Exception;

class TransferException extends \RuntimeException implements GuzzleException {}

しかしこれにもまだ問題がある。

  • TransferException が万が一 GuzzleException でなくなったり、消えたりした場合にテストが落ちる。
  • TransferException 以外の GuzzleException にも対応できているのかがわからない。

ではどうすればいいのか。

解決方法

実は Mockery::mock にはクラスとインタフェースを両方とれたり、インタフェースを複数取れたりする機能があるのでそれを使う。

<?php
public function test() {
    $exceptionMock = \Mockery::mock(\Exception::class, \GuzzleHttp\Exception\GuzzleException::class)
    $this->expectException(MyInternalErrorException::class);
    $this->guzzleClient->shouldReceive('post')
        ->andThrow($exceptionMock);
    // 実際にメソッドが呼んで例外が投げられるかを確認する
}

こうすると、GuzzleException を implements し Exception を継承したクラスが出来上がる。以下のようなイメージだ。

<?php

class MockeryMockException extends Exception implements GuzzleException

これで Exception は thorwable なのでこれで問題は解決だ。( Exception でなくても、 RuntimeException くらいでもいいかもしれない)

まとめ

Mockeryのmockメソッドは複数引数を与えるとインタフェースを実装したクラスをモックしたり、複数のインタフェースを実装したクラスをモックできる!!!

参考

いきなりはじめるPHP~ワクワク・ドキドキの入門教室~

いきなりはじめるPHP~ワクワク・ドキドキの入門教室~

気づけばプロ並みPHP 改訂版--ゼロから作れる人になる!

気づけばプロ並みPHP 改訂版--ゼロから作れる人になる!