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

## 概要

この記事は[DMMグループ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/dmm) 20日目の記事です

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

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

## 内容

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

```php

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

```php

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

```php

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で対応するようにしています。

## 終わりに

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

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

## 参考資料

* [サービスコンテナ](https://readouble.com/laravel/5.5/ja/container.html)