PHP
PHPUnit
sqlite
laravel
lumen

sqliteのin memoryデータベースをテストに使うとrefreshApplication後におかしくなる

はじめに

Laravelのテストでsqliteのメモリ内データベースを使うと、途中でテーブルが存在しないというエラーがでる問題をなんとかしようとして、LumenでsqliteのURI Filenamesを使いたいでは、PDOがそもそもURI filenamesをサポートしてないというオチで玉砕しましたが、やっぱりテストは速い方がいいのでもう少し頑張ってなんとかならないか調べて見ました。

どうしてテーブルが存在しないエラーが出るのか

Laravelは基本的にはデータベースへの接続は使い回してくれるはずで、なぜ新しい接続が作られるのかをちゃんと調べて見ます。

すると、テスト中でrefreshApplicationを行った後にエラーが出る(=新しい接続がつくられる)ことがわかりました。

つまり以下のようなことが起きたと考えられます

  • refreshApplicationが実行されたので、サービスコンテナは空の状態から再出発
  • 作成済みのデータベース接続を管理する\Illuminate\Database\DatabaseManagerもシングルトンとしてサービスコンテナに登録されているが、これに伴って作り直し
  • 既にあったsqliteへの接続は引き継がれていないので次に必要になったタイミングで新たに接続される

わかってみれば当たり前です。普通はDBへの新しい接続がつくられるだけでDBの中身はかわらないから問題がないですが、:memory:の時には空になってしまうからエラーになるわけです。

対策

いろいろな対処方法があるとは思いますが、ここでは一度作ったin memoryデータベースに接続しているPDOをなんとかして使い回せばいいだろうと考えて、SQLiteConnectorを改造するパターンで行きます。

:shared-memory:という特別な名前を用意して、これが指定されたときは最初に作ったPDOを保持して以後それを使い回すようにします。

作成した\App\Database\SqliteConnectorです。

<?php
namespace App\Database;


class SqliteConnector extends \Illuminate\Database\Connectors\SQLiteConnector
{
    protected static $shared_memory_connection = null;

    /**
     * Establish a database connection.
     *
     * databaseが:shared-memory:のときは作成したPDOを保存して以後はそれを返すようにする
     *
     * @param  array  $config
     * @return \PDO
     *
     * @throws \InvalidArgumentException
     */
    public function connect(array $config)
    {
        if ($config['database'] === ':shared-memory:') {
            if (self::$shared_memory_connection === null) {
                $options = $this->getOptions($config);
                self::$shared_memory_connection = $this->createConnection('sqlite::memory:', $config, $options);
            }

            return self::$shared_memory_connection;
        }

        return parent::connect($config);
    }
}

あとはAppServiceProviderregisterメソッドで登録してやります。

    public function register()
    {
        $this->app->bind('db.connector.sqlite', SqliteConnector::class);
    }

.env.testingで新たに使えるようになった:shared-memory:を指定します。

DB_CONNECTION=sqlite
DB_DATABASE=:shared-memory:

おわりに

テストの実行時間はこんな感じです。Docker for macなのでファイルアクセスが遅いのですが、やはりメモリは速いです。

使用したDB 実行時間
MySQL 74.1sec
SQLite(ファイル) 23.4sec
SQLite(メモリ) 7.3sec

refreshApplicationは影響が大きいので使うのはどうかというのはありますが、追加したモックをバッサリ戻すのに便利なんですよね。