🐨

”method”という名前のメソッドがあるクラスをPHPUnitでStubにする方法

2024/03/15に公開

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 stub->expects(this->any())->method('doSomething')->willReturn('foo'); has to be used.

Stubmethodを呼ぶ前に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にしてしまうためにcreateStubcreateMockにしてしまうことですかね。

また、そもそもcreateStubを利用せずFakeオブジェクトや匿名クラスを用意してしまっても問題ないと思います。

$stub = new class implements FugaInterface {
    public function method(): string
    {
        return 'GET';
    }

    public function url(): string
    {
        return 'https://example.com/';
    }
};

まとめ

以上、"method"という名前を持つクラスのスタブ化の方法でした。
まさか自分の書いたメソッド名が衝突するとは思わず、公式がきちんと回避方法を載せていてくれてよかったです。

NE株式会社の開発ブログ

Discussion