[Symfony] LiipTestFixturesBundleを使った機能テストでサービスをモックする時の落とし穴
やや限定的な話ですが、たまたまハマったので備忘録として記録します。
前提
以下のようなケースを考えます。
- LiipTestFixturesBundleを使ってフィクスチャを登録して機能テストしたい
- プロダクトコードには、Entity Listenerなどを使ってPrePersistのタイミングでエンティティのデータを整形する処理がある
- このPrePersist時に呼ばれるデータ整形のためのサービスをモックしたい
シンプルな例だと、エンティティの createdAt
プロパティにPrePersistのタイミングで自動で現在日時を入れるとかが考えられます。このとき、機能テストで任意の createdAt
を持たせたエンティティをフィクスチャで登録したいと思ったら、「エンティティの createdAt
に現在日時を入れるサービス」をテスト時にだけ「何もしないサービス」で置き換える(モックする)という解法が考えられます。
LiipTestFixturesBundleを使った機能テストの実践方法については こちらの過去記事 に詳しくまとめています。
DoctrineのEntity Listenerについては こちらの過去記事 に詳しくまとめています。
実際のやり方
これを実際にテストコードで実装する場合、以下のようになると思います。
class FooControllerTest extends WebTestCase
{
use FixturesTrait;
protected function setUp(): void
{
self::getContainer()->set('サービス名', new NopService());
$this->loadFixtureFiles([
__DIR__.'/../fixtures/Controller/FooControllerTest.yaml',
]);
}
public function testSomeAction()
{
// ...
}
}
loadFixtureFiles()
する前に、Entity Listenerから呼ばれるであろうサービスを、何もしないサービスに置き換えていますね。
落とし穴
上記のコードで何も問題はないのですが、サービスを置き換えるためにサービスコンテナを取得するところで、地味にハマりポイントがあります。
というのも、
- https://symfony.com/doc/current/testing.html#accessing-the-container
- https://symfony.com/blog/new-in-symfony-4-1-simpler-service-testing
このあたりの公式ドキュメントを見てサービスコンテナにアクセスしようと思うと、
self::$container->set('サービス名', new NopService());
や
$kernel = self::bootKernel();
$kernel->getContainer()->set('サービス名', new NopService());
といったコードを書いてしまいそうになりますが、実はこれだと 置き換え前のサービスが呼ばれてしまいます。
何故でしょうか。
実は大変紛らわしいのですが、
という異なる2つのサービスコンテナがある状態なのです。
LiipTestFixturesBundleがフィクスチャをpersistするために使うサービスコンテナは後者なので、こっちのサービスコンテナでサービスをモックしないと意味がないというわけです。
なので、先に書いたとおり
self::getContainer()->set('サービス名', new NopService());
この方法でモックするのが正解です。罠ですね。
まとめ
- LiipTestFixturesBundleを使った機能テストにおいてPrePersistで呼ばれるサービスをモックするときは、
KernelTestCase
が持っているサービスコンテナではなく、FixturesTrait
が持っているサービスコンテナでサービスをモックしないといけないので要注意
Discussion