🎻

[Symfony] 再利用可能なバンドルを機能テストする方法(2021年版)

2021/12/05に公開

Symfony Advent Calendar 2021 の5日目の記事です!🎄🌙

ちなみに、僕はよく TwitterにもSymfonyネタを呟いている ので、よろしければぜひ フォローしてやってください🕊🤲

昨日は @77web さんの PHP8.1でSymfony6のEnumTypeを使って遊んでみた でした✨

再利用可能なバンドルとは?

Symfonyの公式ドキュメントでは、不特定のSymfonyアプリケーションにインストールして使ってもらうための配布用のバンドルのことを、特定のSymfonyアプリケーションの内部に作るバンドルと区別して 「再利用可能なバンドル(reusable bundles)」 と呼んでいます。

再利用可能なバンドルの機能テストとは?

もし再利用可能なバンドルが単体テストでしかテストされていないと、実際にSymfonyアプリケーションにインストールされた後で問題が顕在化する可能性が多分にあります。

そこで、再利用可能なバンドルを作って配布する場合は、単体テストだけでなく、実際にSymfonyにインストールした状態で動かして結果を検証する機能テストも書いておけるとより安心です。

再利用可能なバンドルを機能テストする方法

機能テストする方法の大まかな流れは

  • Symfony\Component\HttpKernel\Kernel を継承したテスト用のKernelを作る
  • テスト用Kernelにバンドルをインストールする
  • テスト用のフレームワーク設定をテスト用Kernelにロードさせる
  • テスト用Kernelを使って機能テストを実装する

という感じになります。

以下、具体的な実装の方法について解説していきます。

実際のコードの例として、拙作の ttskch/paginator-bundle のテストコードへのリンクを要所で併記ます✋

Symfony\Component\HttpKernel\Kernel を継承したテスト用のKernelを作る

まず、Symfony\Component\HttpKernel\Kernel を継承したテスト用のKernelを作ります。

use Symfony\Component\HttpKernel\Kernel;

class TestKernel extends Kernel
{
}

テスト用Kernelにバンドルをインストールする

次に、このテスト用Kernelに FrameworkBundle とテスト対象である再利用可能なバンドルをインストールします。

use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\HttpKernel\Kernel;

class TestKernel extends Kernel
{
    use MicroKernelTrait;

    public function registerBundles(): iterable
    {
        return [
            new FrameworkBundle(),
            new \Your\OwnBundle(),
        ];
    }
}

Kernelに対してバンドルやルーティング、サービスコンテナの設定を行うためのメソッド群は MicroKernelTrait というトレイトに分離されている ので、これを use して registerBundles() メソッドを上書きしています。

テスト用のフレームワーク設定をテスト用Kernelにロードさせる

さらに、テスト用のフレームワーク設定を test.yaml routes.yaml に記述しているとして、それらをテスト用Kernelにロードさせます。

use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;

class TestKernel extends Kernel
{
    use MicroKernelTrait;

    public function registerBundles(): iterable
    {
        return [
            new FrameworkBundle(),
            new \Your\OwnBundle(),
        ];
    }

    protected function configureContainer(ContainerConfigurator $c)
    {
        $c->import('/path/to/test.yaml');
    }

    protected function configureRoutes(RoutingConfigurator $routes)
    {
        $routes->import('/path/to/routes.yaml');
    }
}

ここでも、 MicroKernelTrait が持っている configureContainer() configureRoutes() メソッドを上書きすることで、サービスコンテナとルーティングの設定をロードさせています。

実際のコード

テスト用Kernelを使って機能テストを実装する

これでテスト用Kernelは完成なので、あとはこのテスト用Kernelを使って機能テストを実装するだけです。

そのための準備として、機能テストの基底クラスとなる WebTestCase を継承して、Kernelを差し替えた独自の WebTestCase を作成します。

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase as BaseWebTestCase;
use Symfony\Component\HttpKernel\Kernel;

class WebTestCase extends BaseWebTestCase
{
    protected static function getKernelClass(): string
    {
        return TestKernel::class;
    }
}

このように、WebTesetCase::getKernelClass() メソッドを上書きすることで、使用するKernelを差し替えることができます。

あとは、こちらの WebTestCase を使って、いつもどおり機能テストを書けばOKです。

class YourFunctionalTest extends \Your\Own\WebTestCase
{
    public function testSomething()
    {
        // test something
    }
}

簡単ですね!

実際のコード

このままだとSymfony 5.0以下の環境でテストできない

さて、以上の方法で再利用可能なバンドルを機能テストすることができるのですが、実はこのままだとSymfony 5.0以下の環境ではテストが正常に実行できません。

例えば、テスト対象の再利用可能なバンドルの composer.json

"require": {
    "symfony/framework-bundle": "^5.0|^6.0"
}

となっている場合に、

$ composer update --prefer-lowest
$ ./vendor/bin/phpunit

のようにしてlowest dependenciesがインストールされた状態でテストを実行すると、エラーになります。

なぜエラーになるのか

テスト用Kernelにおいて、MicroKernelTrait に定義されている configureContainer() configureRoutes() メソッドを上書きすることでサービスコンテナとルーティングの設定をロードさせましたが、実はこれらのメソッド、Symfony 5.0→5.1のタイミングでメソッドシグネチャが変更されているのです😓

具体的には以下の2箇所です。

このBC Breakにより、Symfony 5.0以下では先ほどのテスト用Kernelは動作しません。

対応策:Symfony 5.0以下用と5.1以上用それぞれのテスト用Kernelを作って使い分ける

この問題を回避するには、Symfony 5.0以下用と5.1以上用それぞれのテスト用Kernelを作って、Symfonyのバージョンに応じてどちらを使うかを選択するようにすればよいです。

具体的には、例えば TestKernelForBC といったKernelクラスを追加で作って、configureContainer() configureRoutes() メソッドをSymfony 5.0以下用のメソッドシグネチャに合わせて実装します。

use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;

class TestKernelForBC extends Kernel
{
    use MicroKernelTrait;

    public function registerBundles(): iterable
    {
        return [
            new FrameworkBundle(),
            new \Your\OwnBundle(),
        ];
    }

    protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader) // Symfony 5.1以上と引数の数が異なる
    {
        $loader->import('/path/to/test.yaml');
    }

    protected function configureRoutes(RouteCollectionBuilder $routes) // Symfony 5.1以上と引数の型が異なる
    {
        $routes->import('/path/to/routes.yaml');
    }
}

そして、独自 WebTestCase において、Symfonyのバージョンに応じて使用するテスト用Kernelを切り替えるようにします。

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase as BaseWebTestCase;
use Symfony\Component\HttpKernel\Kernel;

class WebTestCase extends BaseWebTestCase
{
    protected static function getKernelClass(): string
    {
        return Kernel::VERSION_ID >= 50100 ? TestKernel::class : TestKernelForBC::class;
    }
}

これで、Symfony 5.0以下の環境でも機能テストが正常に実行可能になります👌

ttskch/paginator-bundleで対応した際のコミットはこちらです。

test: 💍 enable to test also with Symfony 5.0- · ttskch/TtskchPaginatorBundle@5789bab

Symfony 4.2.3以下では既知のバグによって機能テストが動作しない

以上の方法で、Symfony 5.0以下でも正常に機能テストが実行できるようになるのですが、Symfony 4.2.3以下の環境だと、KernelTestCase クラスのメソッドシグネチャがPHPUnit 8のそれと互換していないという既知のバグ があるため実行できません。

Symfony 4.1は2019年1月ですでにBug fix supportが終了しているためこのバグの修正は取り込まれていません。

なので、テスト対象の再利用可能なバンドルがSymfony 4にも対応している場合は、composer.json

"require": {
    "symfony/framework-bundle": "^4.2.4|^5.0|^6.0"
}

のように4.2.4以上を明示的に要求する必要があります。

参考サイト

おわりに

というわけで、Symfonyで再利用可能なバンドルを機能テストする方法と、一部注意が必要な点について解説しました。

本題とは関係ありませんが、実際のテストコードの例として紹介した ttskch/paginator-bundle は、Symfonyでページネーションを実装するための最も軽量で最も柔軟なバンドル(手前味噌)だと思います😇(つい先日 Symfony 6にも対応しました👍)

細かな使い方など以下の過去記事で詳しく解説しているので、よろしければぜひ試してみていただけると嬉しいです!

[Symfony] シンプルでカスタマイズしやすい最強のページネーションバンドル

Symfony Advent Calendar 2021、明日は @ippey_s さんです!お楽しみに!

GitHubで編集を提案

Discussion