8
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

PHPの便利なモックオブジェクトフレームワーク「Mockery」のケーススタディ

Last updated at Posted at 2019-12-19

はじめに

Mockeryのケーススタディをつらつらと書いていきます。
こういうケースでは、このようにコードを書いたら良さげ というように書いていきます。
日本語で書かれてるドキュメントがあります。感謝しかない。

ケーススタディ

クラスAのテストをしたいけど、クラスAはクラスBに依存してて、クラスBの影響を受けたくない

よくあるパターンですね。
クラスAはクラスBに依存している。(クラスAはクラスBが無いと動かない)
クラスAの単体テストを書きたいので、クラスBがうまく動いてなくてもクラスAのテストに影響を与えたくない。

サンプルコード

class A
{
    private $classB;

    public function __construct(B $classB)
    {
        $this->classB = $classB;
    }

    /**
     * foobarを取得する
     *
     * @return string
     */
    public function fooBar(): string
    {
        $bar = $this->classB->methodBar();
        return 'foo' . $bar;
    }
}

class B
{
    public function methodBar(): string
    {
        // ここにバグがあったらクラスAのテストが失敗する。
        return 'bar';
    }
}

Mockeryを使ったテストコード

class Test_A extends TestCase
{
    public function testFooBar()
    {
        /** @var B $stubB クラスBの代替オブジェクト */
        $stubB = Mockery::mock(B::class);
        // 実際のクラスBのmethodBarメソッドにバグがあっても、クラスAのテストに影響しない。
        // クラスAのテストはクラスBに依存しなくなった!
        $stubB->shouldReceive('methodBar')->andReturn('bar');

        $classA = new A($stubB);
        $this->assertEquals('foobar', $classA->fooBar());
    }
}

クラスAのテストをしたいけど、クラスBのメソッドをstaticに呼んでいてモックに入れ替えられない

メソッドの中で別のクラスのメソッドをstaticに呼んでるからモックに置き換えられない!!!
強い依存が発生してますね。
Facadeや依存注入で置き換えられたらいいんですが、そのような仕組みなってないこともあると思います。どうしたらいいんだ・・・

サンプルコード

class A
{
    /**
     * foobarを取得する
     *
     * @return string
     */
    public function fooBar(): string
    {
        // 強い依存が発生!モックに置き換えられない
        $bar = B::methodBar();
        return 'foo' . $bar;
    }
}

class B
{
    public static function methodBar(): string
    {
        return 'bar';
    }
}

Mockeryを使ったテストコード

そんなときはaliasの出番です。
制約1はありますが、置き換えられます。

class Test_A extends TestCase {

    public function testFooBar()
    {
        // Bクラスの定義自体をスタブに置き換える
        $stubB = Mockery::mock('alias:'. B::class);
        $stubB->shouldReceive('methodBar')->andReturn('bar');

        $classA = new A($stubB);
        $this->assertEquals('foobar', $classA->fooBar());
    }
}

クラスAのテストをしたいけど、クラスAはクラスBに強く依存している(密結合になっている)

メソッドの中で依存クラスをnewしてて、差し替え不能。強い依存が発生している・・・
上のstaticを呼んでるパターンと似ていますね。
うーん、つらい。
こんなときもなんとか置き換えられます。制約はstaticの時と同じです。

サンプルコード

class A
{
    /**
     * foobarを取得する
     *
     * @return string
     */
    public function fooBar(): string
    {
        // 強い依存発生!
        $classB = new B();
        $bar = $classB->methodBar();
        return 'foo' . $bar;
    }
}

class B
{
    public function methodBar(): string
    {
        return 'bar';
    }
}

Mockeryを使ったテストコード

class Test_A extends TestCase {

    public function testFooBar()
    {
        // こうすることで、new B()で生成されるインスタンスがMockeryのオブジェクトになります
        $stubB = Mockery::mock('overload:'. B::class);
        $stubB->shouldReceive('methodBar')->andReturn('bar');

        $classA = new A($stubB);
        $this->assertEquals('foobar', $classA->fooBar());
    }
}

※これもstaticの時と同様に、先に実際のクラスをロードしていたらできません。オートロードするように修正しましょう。

クラスAのテストをしたいけど、クラスAはインターフェースZに依存してる

サンプルコード

いいですね!抽象に依存しているので差し替えが楽です。

class A
{
    /** @var InterfaceZ */
    private $interfaceZ;

    public function __construct(InterfaceZ $classB)
    {
        $this->interfaceZ = $classB;
    }

    /**
     * foobarを取得する
     *
     * @return string
     */
    public function fooBar(): string
    {
        $bar = $this->interfaceZ->methodBar();
        return 'foo' . $bar;
    }
}

class B implements InterfaceZ
{
    public function methodBar(): string
    {
        return 'bar';
    }
}

interface InterfaceZ
{
    public function methodBar(): string;
}

Mockeryを使ったテストコード

この場合は、mockメソッドにインターフェース名を渡してあげればいい感じになります。

class Test_A extends TestCase {

    public function testFooBar()
    {
        // ここでインターフェース名を指定
        $stubB = Mockery::mock(InterfaceZ::class);
        $stubB->shouldReceive('methodBar')->andReturn('bar');

        $classA = new A($stubB);
        $this->assertEquals('foobar', $classA->fooBar());
    }
}

クラスAのテストをしたいけど、中でメソッドチェーンが発生していて色々な別のクラスのメソッドを利用している(デメテルチェーンがある)

サンプルコード

クラスが依存しているクラスが所持している別のクラスのメソッドを呼ぶこともあるでしょう。
あまり良くない構造ではありますが・・・(クラスAが直接依存しているクラスB以外の関心事を知ってしまっている)
クラスCのモックも作ってがんばる方法もありますが、面倒ですね。

class A
{
    private $classB;

    public function __construct(B $classB)
    {
        $this->classB = $classB;
    }

    /**
     * foobazを取得する
     *
     * @return string
     */
    public function fooBaz(): string
    {
        // デメテルチェーン発生。もしクラスD、クラスEがあったらゾッとしますね
        $bar = $this->classB->getC()->methodBaz();
        return 'foo' . $bar;
    }
}

class B
{
    private $classC;

    public function __construct(C $classC)
    {
        $this->classC = $classC;
    }

    public function getC()
    {
        return $this->classC;
    }
}

class C
{
    public function methodBaz()
    {
        return 'baz';
    }
}

Mockeryを使ったテストコード

shouldReceiveには method1->method2 という書き方ができます。
クラスCのモックまで作らなくても良くなります。

class Test_A extends TestCase {

    public function testFooBaz()
    {
        /** @var B $stubB クラスBの代替オブジェクト */
        $stubB = Mockery::mock(B::class);
        // デメテルチェーンが発生していても、モックオブジェクト沢山作らなくて済む
        $stubB->shouldReceive('getC->methodBaz')->andReturn('baz');

        $classA = new A($stubB);
        $this->assertEquals('foobaz', $classA->fooBaz());
    }
}

クラスAのテストで、依存しているクラスBを期待した条件で利用していることを確認したい

サンプルコード

イメージしやすくなると思って、クラスBをMailerとしています。
クラスAのsendMailWithSignature()って、正しくMailerを使ってるの? を確かめるテスト書きたい。

class A
{
    private $mailer;

    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function sendMailWithSignature($mailaddress, $body): void
    {
        $body .= ' signature!!!';
        $this->mailer->send($mailaddress, $body);
    }
}

class Mailer
{
    public function send(string $mailaddress, string $body): string
    {
        // 実際にはメールを送信する処理
        return 'メール送ったよ';
    }
}

Mockeryを使ったテストコード

with()メソッドやonce()メソッドで、期待している引数や呼ばれる回数を指定できます。
メール1通送るつもりがバグで100通送っちゃった!のようなことが無くなりますね。

class Test_A extends TestCase {

    public function tearDown()
    {
        parent::tearDown();
        Mockery::close();
    }
    
    public function testFooBar()
    {
        $mailaddress = 'foo@bar.baz';
        $body = 'mail body';

        $mockMailer = Mockery::mock(Mailer::class);
        // sendメソッドに、第1引数が「foo@bar.baz」、第2引数が「mail body signature!!!」で
        // 1回だけ実行されるはず
        $mockMailer->shouldReceive('send')
            ->with($mailaddress, $body . ' signature!!!')
            ->once();

        $classA = new A($mockMailer);
        $classA->sendMailWithSignature($mailaddress, $body);
    }
}

おわりに

これらのようなテスト対象が依存しているものの代替品のことをテストダブルといいます。
テスト対象以外の影響を受けなくなり、本当にテストしたいものに集中できる利点があります。
また、テストダブルを念頭に置いてクラス設計することで、クラスの責務を意識するようになり、神クラスを作りづらいという利点もあると思っています。
なんでもできる神クラスは作りたくないものですね。

  1. ※クラスの定義自体をMockeryで作成したものに置き換えるので、オートロードが機能していて、かつ本当のクラスBをロードしていない状態でないと利用できません。require_once('classes/classB.php') とか手動でロードしてるとダメです。

8
7
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
8
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?