12
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.

僕たちとLaravelプロジェクトのテスト変遷〜Mockを差したい〜

Last updated at Posted at 2019-12-19

概要

この記事はDMMグループ Advent Calendar 2019 20日目の記事です

DMM.comでサーバサイドエンジニアをやっているjuve534です。
現在は配信基盤のリプレイスプロジェクトにて、PHP×Laravelを使ったAPI開発やAWSを使ったインフラ構築を行っています。

今回は、自分たちのAPI開発における、テストコードの変遷を紹介したいと思います。

内容

バージョン1

プロジェクトを始めた当初は、ServiceクラスやRepositoryのテストはインスタンスを new して、テストを書いていました。
コンストラクタインジェクションを使っているので、インジェクションするクラスはMockeryを使い、下記のように書いていました。


class HogeTest extend TestCase
{
    public function testHoge($id)
    {
        // setup
        $mockFogaRepository = Mockery::mock(fugaRepository::class);
        $mockFogaRepository->shouldReceive('何かしら')
            ->once()
            ->with($id)
            ->andReturn();

        // exercise
        $hogeService = new hogeService(
            $mockFogaRepository
        );

        // verify
        $actual = $hogeService->hoge($id);
        $this->assertTrue($actual);
    }
}

この書き方で開発を行っていたのですが、コード量が増えると苦しくなってきました。
コンストラクタインジェクションが増えれば、 HogeTest のテストコードをすべて書き直す必要があるためです。

先程の例でいうと、 hogeService に新しくコンストラクタインジェクションを増やした場合、 testHoge のテストはコケてしまいます。
1ケースなら対応できますが、テストコードによっては100ケース(パラメタライズテスト込み)もあったりするので、コンストラクタインジェクションが増えるたびに書くのは、時間もかかりました。
しかも、ただクラスを生成して渡してあげる修正になるので、時間がかかることと合わせ、精神ゲージが削られました。

そこで、下記のようにテストを書き直していきました。

バージョン2


class HogeTest extend TestCase
{
    public function testHoge($id)
    {
        // setup
        $this->app->bind(fugaRepositoryInterface::class, function () use ($id)) {
            $mock = Mockery::mock(fugaRepository::class);
        $mock->shouldReceive('何かしら')
            ->once()
            ->with($id)
            ->andReturn();

            return $mock;
        });

        // exercise
        $hogeService = resolve(hogeServiceInterface::class);

        // verify
        $actual = $hogeService->hoge($id);
        $this->assertTrue($actual);
    }
}

意識したポイントはサービスコンテナで依存性を解決することでした。
resolve メソッドは、クラスインスタンスを依存解決してくれるので、仮にコンストラクタインジェクションが増えたとしても、サービスコンテナで依存性を吸収してくれました。
モックを差したい処理は、$this->app->bind でモックを返してあげることで、モックを差すことができました。

結果として、下記2つの利点を享受することができ、少しだけハッピーになることができました。

  1. コンストラクタインジェクションが増えても、テストケースを書き直す必要が減った
  2. バージョン1に比べ、不要なモックを刺すことも減った

これで万事解決といいたいところですが、開発が進むとまた問題が出てきました。

バージョン3


class HogeTest extend TestCase
{
    public function testHoge($id)
    {
        // setup
        $this->app->singleton(fugaRepositoryInterface::class, function () use ($id) {
            $mock = Mockery::mock(fugaRepository::class);
        $mock->shouldReceive('何かしら')
            ->once()
            ->with($id)
            ->andReturn();

            return $mock;
        });

        // exercise
        $hogeService = resolve(hogeServiceInterface::class);

        // verify
        $actual = $hogeService->hoge($id);
        $this->assertTrue($actual);
    }
}

先程との違いは、モックを差す際に、bindからsingletonに変わったことです。
bindは同じインスタンスを返してくれないため、依存関係が複雑になると、モックではなく実体を返してしまうことがありました。
singletonは依存関係の解決を1度にしてくれるため、必ずモックを返してくれました。
そのため、bindでコケた際は、singletonに書き直すようにしました。

※bindでコケたことは自分たちのコードが複雑になっているバロメーターであり、本来は依存関係を正すことが重要だと思います。ただ、現状はそこに手を付けられていないため、singletonで対応するようにしています。

終わりに

自分たちのテストコードの変遷は以上となります。
テストコードで詰まっている人の、少しでも参考になれば幸いです。

最後まで読んでいただきありがとうございました。

参考資料

12
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
12
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?