”method”という名前のメソッドがあるクラスをPHPUnitでStubにする方法
NE株でPHPを書いている谷口(@taniguhey)です。
テストコードを書いていて、VSCodeが妙な赤線を吐くな〜と思っていたら実はレアケースな仕様を踏んでいたのでその紹介です。
タイトルの通りPHPUnitで"method"と言う名前のメソッドを持つクラスをスタブにしようとしたときに、通常のスタブの作り方では上手くいかなかったというお話を、サンプルコードを交えて書いていきます。
環境
- PHPUnit 9.6.15
物語
以下のような、REST APIを表すインターフェースを設計しているとします。
interface FugaInterface
{
/**
* HTTPメソッドを返す
*
* @return string
*/
public function method(): string;
/**
* URLを返す
*
* @return string
*/
public function url(): string;
// ...
}
このインターフェースに依存しているクラス(例えば外部サービスのAPIを呼び出すクラス)のユニットテストを書きたかったとします。
具象クラスではなくスタブを作って注入しようとしても、以下のようなテストコードは正しくStub
オブジェクトを作れずエラーになってしまいます。
public function testHoge_Fugaリクエストをすること(): void
{
$stub = $this->createStub(FugaInterface::class);
$stub->method('method')
->willReturn('GET');
$subject = new Sut($stub);
self::assertSame('GET', $subject->hoge());
}
createStub
の戻り値のクラスは元のクラスのメソッドもStub
クラスのメソッドも兼ね備えているため、method
の返す値を決めるためのmethod
が衝突しています。
衝突自体はエラーにならずに、method
はオリジナルクラスのものが優先されてしまうため、この場合stringに対してwillReturn
が呼び出せないというエラーになります。
Error: Call to a member function willReturn() on string
どうするべきか
こういったレアケースも公式のドキュメントできちんと補足されています(素晴らしい)。
The example shown above only works when the original class does not declare a method named “method”.
If the original class does declare a method named “method” then
this->any())->method('doSomething')->willReturn('foo'); has to be used. stub->expects(
Stub
のmethod
を呼ぶ前にexpects($this->any())
を挟むこととなっています。
public function testHoge_Fugaリクエストをすること(): void
{
$stub = $this->createStub(FugaInterface::class);
$stub->expects($this->any()) // ←これを足す
->method('method')
->willReturn('GET');
$subject = new Sut($stub);
self::assertSame('GET', $subject->hoge());
}
ちなみに
createStub
で作ったオブジェクトは、オリジナルクラスと\PHPUnit\Framework\MockObject\Stub
の交差型となっているので、PHPStanなどの静的解析で存在しないexpects
を呼び出していると怒られてしまいます。
一番楽に回避するには、expects
を実装しているMockObject
にしてしまうためにcreateStub
をcreateMock
にしてしまうことですかね。
また、そもそもcreateStub
を利用せずFakeオブジェクトや匿名クラスを用意してしまっても問題ないと思います。
$stub = new class implements FugaInterface {
public function method(): string
{
return 'GET';
}
public function url(): string
{
return 'https://example.com/';
}
};
まとめ
以上、"method"という名前を持つクラスのスタブ化の方法でした。
まさか自分の書いたメソッド名が衝突するとは思わず、公式がきちんと回避方法を載せていてくれてよかったです。
NE株式会社のエンジニアを中心に更新していくPublicationです。 NEでは、「コマースに熱狂を。」をパーパスに掲げ、ECやその周辺領域の事業に取り組んでいます。 Homepage: ne-inc.jp/
Discussion