🥥

EC CUBEのPHPUnitテストをインメモリDBのSQLiteで実行する

2023/06/10に公開2

タイトルの通りですが、インメモリのデータベースを使ってテストを実行します。
最初、ググった通りにやっても上手くいかなくて苦戦したので、備忘録がてら手順をまとめていきます。

対象者

  • EC CUBEのPHPUnitテストを、インメモリのデータベースを使って実行したい方
  • 一部、EC CUBE固有のファイルが出てきますが、Symfonyを使っていれば大体同じ手順でできると思います

動作環境・前提

バージョン
Mac 12.x
PHP 7.4.33
SQLite 3.37.0
Symfony 5.4.21
EC CUBE 4.x

早速実装していく

doctrine.yaml

まず、今回最も重要になるdoctrine.yamlファイルを編集していきます。
ここでは、ドライバーやデータベースのURLなどの設定を行います。
ここが間違っていたら何もかも上手くいかないです。

app/config/eccube/packages/test/doctrine.yaml
doctrine:
    dbal:
        connections:
            default:
                driver: 'pdo_sqlite'
                charset: utf8
                url: 'sqlite:///:memory:'
                dbname: 'test_db'

細かく話すとめちゃくちゃ長くなるのでざっくり説明すると、driver: 'pdo_sqlite'でSQLiteを使うこと、url: 'sqlite:///:memory:'でインメモリのデータベースを使うことを指定しています。
詳しくは公式サイトを見ていただくのがいいと思いますので、以下を参考にしてください。

phpunit.xml

続いて、phpunit.xmlを編集します。
編集するのは以下の部分です。

phpunit.xml
<!-- 省略 -->

    <php>
        <ini name="error_reporting" value="-1" />
        <env name="KERNEL_CLASS" value="Eccube\Kernel" />
        <env name="APP_ENV" value="test" force="true"/>
        <env name="APP_DEBUG" value="1" />
        <env name="SHELL_VERBOSITY" value="-1" />
        <env name="SYMFONY_DEPRECATIONS_HELPER" value="weak" />
        <!-- 以下を追加 -->
        <env name="DATABASE_URL" value="sqlite:///:memory:"/>
        <!-- define your env variables for the test env here -->
    </php>

<!-- 省略 -->

ここでもdoctrine.yamlと同じくDBのURLを記述しておく必要があります。
他はデフォルトのままでも大丈夫だと思います。

また、上記の例でenvとしている部分が、serverとなっている場合もあるようです。
こちらは、ご自身の環境に合わせて書き換えてください。

ここまでできたら、テストでインメモリのDBを使うことができるようになっています。
DBを使うテストが実装済みの方は、このタイミングで一度実行してみてください。

...実行できたでしょうか?
おそらくこの時点だとエラーまみれになりますが、それで正常です。
(逆にエラーにならなかった場合、もしかしたらここまでの手順にミスがあるかもしれません。)

EccubeTestCase.php

先ほどテストを実行した際、エラーメッセージのほとんどが「DBがありません」とか「テーブルがありません」みたいな内容だったのではないでしょうか?
それもそのはず。まだデータベースを作成するコードを書いていませんからね。

ということで、テストの前にデータベースを作成し、スキーマを更新するコードを書いていきます。

ここで注意してほしいのは、ターミナルでデータベースを作成するコマンドを実行しても無駄、ということです。
なぜなら、インメモリのデータベースは、プロセスが終了すると削除されてしまうからです。

こういうこと
$ symfony console doctrine:database:create --env=test
# ↓この時点でプロセスは終了している(DBが作られた次の瞬間、そのDBが削除される)
Created database "test_eccubedb" for connection named default

$ vendor/bin/phpunit --testsuite TestSuite
# 新しいプロセスになるので、エラーになる
EEEEEE

ではどうするのかというと、テストのコードを実行する直前(=同じプロセスの中)で、テスト用のデータベースを初期化するコードをPHPで書くことにします。

以下は、EccubeTestCase.php(名前の通りですが、こちらがEC CUBE固有のファイルです)の一部です。
データベースを扱うテスト全てにこのクラスを継承させることを前提として、setUp()メソッドの中でデータベースを初期化するコードを追記していきます。
先述した2つのファイルでインメモリのデータベースを使うように設定してあるので、ここではその辺りの設定をする必要はありません。勝手にテスト用のデータベースを使ってくれます。

では、まずデータベースを初期化するメソッドを実装します。
こんな感じ。

tests/Eccube/Tests/EccubeTestCase.php
use Doctrine\ORM\Tools\SchemaTool;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Bundle\FrameworkBundle\Console\Application;

private function initDatabase(KernelInterface $kernel): void
{
    // スキーマの更新
    $entityManager = $kernel->getContainer()->get('doctrine.orm.entity_manager');
    $metaData = $entityManager->getMetadataFactory()->getAllMetadata();
    $schemaTool = new SchemaTool($entityManager);
    $schemaTool->updateSchema($metaData);

    $application = new Application($kernel);
    $application->setAutoExit(false);

    // フィクスチャーをロード
    $input = new ArrayInput(['command' => 'eccube:fixtures:load']);
    $output = new BufferedOutput();
    $application->run($input, $output);
}

Symfony、Doctrineの標準機能を使ってデータベースの初期化を行っています。
それぞれの機能については、公式ドキュメントを読むのが早いと思うので、詳しく知りたい方は以下のサイトを参考にしてください。

公式ドキュメントではないですが、以下のサイトも参考になったので紹介しておきます。

「詳細はいいからとりあえず動くとこまでやりたい」という方は、一旦このまま先に進んでもらって大丈夫です。

このメソッドを、setUp()メソッドで呼び出します。

tests/Eccube/Tests/EccubeTestCase.php
public function setUp()
{
    parent::setUp();
    $this->client = static::createClient();

    $kernel = self::bootKernel();
    // ここで呼び出す
    $this->initDatabase($kernel);

    $this->entityManager = self::$container->get('doctrine')->getManager();
    $this->eccubeConfig = self::$container->get(EccubeConfig::class);
}

これでそれぞれのテストが実行される直前にデータベースが作れられるようになりました。
早速、このクラスを継承したテストを実装していきましょう。

DoctrineTest.php
use Eccube\Tests\EccubeTestCase;

class DoctrineTest extends EccubeTestCase
{
    public function setUp()
    {
        parent::setUp();
    }

    public function test(): void
    {
        // 任意のテストコード
    }
}

任意のテストコードの部分には、データベースとのやり取りを含むコードを実装してみてください(データベース使わないテストだと上手くいっているか判断できないので)。

では、再度ターミナルなどを起動して、このテストを実行してみてください。
今度は「DBがありません」や「テーブルがありません」のようなエラーは出なくなっているはずです。
もしエラーが出てしまった方は、doctrine.yamlにミスがないか、initDatabase()の実装にミスがないか、setUp()メソッドで呼び出せているか、継承するクラスを間違えていないか等々、確認してみてください。

テストが無事にパスしたら成功です!

おまけ

ちなみに、このテストはGithubActionsでもちゃんとパスします。

以下は、私が実際に書いたGithubActionsのコードの一部です。

.github/workflows/php-unit.yaml
# 省略
steps:
    - name: Setup PHP 7.4.3
    uses: shivammathur/setup-php@master
    with:
        php-version: '7.4.3'
        tools: phpunit
    
    - name: Checkout
    uses: actions/checkout@master

    - name: Composer cache clear
    id: composer-cache
    run: |
        echo "::set-output name=dir::$(composer config cache-files-dir)"
    - uses: actions/cache@v1
    with:
        path: ${{ steps.composer-cache.outputs.dir }}
        key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
        restore-keys: |
        ${{ runner.os }}-composer-

    - name: Composer Install
    run: composer update --dev --no-interaction -o --apcu-autoloader

    - name: Run test suite
    run: vendor/bin/phpunit --testsuite TestSuite

ブログ用に簡略化して書きましたが、こんな感じのGAを使っています。

参考にしたサイト

カラビナテクノロジー デベロッパーブログ

Discussion

Kentaro OhkouchiKentaro Ohkouchi

以前、EC-CUBE本体でもインメモリで実行しようと取り組んだのですが、性能差がほとんど無かったのと、 PostgreSQL/MySQLと同一のテストコード動作しないため、現在の実装になっています。
もしインメモリの方が速いようであれば採用していきたいのですが、実際のところいかがでしょうか?

ろみぃ(konatsu)ろみぃ(konatsu)

このテストを使っていた環境が現在手元にないため記憶ベースにはなるんですが、仰る通り性能差はほぼなかったと思います。

GAでのテスト実行が少しでも早くならないかと試しにインメモリでやってみた、という背景があるんですが、気持ち早くなったかな、くらいで大きな変化はなかったと記憶しています。