🎻

[Symfony] LiipTestFixturesBundleを使った機能テストでサービスをモックする時の落とし穴

2020/05/11に公開

やや限定的な話ですが、たまたまハマったので備忘録として記録します。

前提

以下のようなケースを考えます。

  • 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から呼ばれるであろうサービスを、何もしないサービスに置き換えていますね。

落とし穴

上記のコードで何も問題はないのですが、サービスを置き換えるためにサービスコンテナを取得するところで、地味にハマりポイントがあります。

というのも、

このあたりの公式ドキュメントを見てサービスコンテナにアクセスしようと思うと、

self::$container->set('サービス名', new NopService());

$kernel = self::bootKernel();
$kernel->getContainer()->set('サービス名', new NopService());

といったコードを書いてしまいそうになりますが、実はこれだと 置き換え前のサービスが呼ばれてしまいます。

何故でしょうか。

実は大変紛らわしいのですが、

  • WebTestCase (の派生元である KernelTestCase )が持っているサービスコンテナ(これ
  • FixturesTrait が持っているサービスコンテナ(これ

という異なる2つのサービスコンテナがある状態なのです。

LiipTestFixturesBundleがフィクスチャをpersistするために使うサービスコンテナは後者なので、こっちのサービスコンテナでサービスをモックしないと意味がないというわけです。

なので、先に書いたとおり

self::getContainer()->set('サービス名', new NopService());

この方法でモックするのが正解です。罠ですね。

まとめ

  • LiipTestFixturesBundleを使った機能テストにおいてPrePersistで呼ばれるサービスをモックするときは、 KernelTestCase が持っているサービスコンテナではなく、 FixturesTrait が持っているサービスコンテナでサービスをモックしないといけないので要注意
GitHubで編集を提案

Discussion