概要
この記事は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に比べ、不要なモックを刺すことも減った
これで万事解決といいたいところですが、開発が進むとまた問題が出てきました。
バージョン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で対応するようにしています。
終わりに
自分たちのテストコードの変遷は以上となります。
テストコードで詰まっている人の、少しでも参考になれば幸いです。
最後まで読んでいただきありがとうございました。