Featured image of post 用 PDO::exec 載入 Schema 讓 Laravel 測試快 7 倍

用 PDO::exec 載入 Schema 讓 Laravel 測試快 7 倍

透過 PDO::exec 把 schema dump 直接載入 SQLite in-memory database,跳過逐一跑 migration 的瓶頸,實測從 2 分 21 秒降到 18 秒。

隨著 migration 檔案越來越多,測試速度越來越慢,即使用 SQLite In-Memory Database 也一樣,因為 migration 是一個檔案一個檔案跑的。

Migration 逐步執行 vs Schema Dump 一次載入的速度差異

schema:dump 不支援 In-Memory Database

Laravel 8 提供了 php artisan schema:dump 指令,能把所有 migration 合併成一個 SQL 檔案。但查看原始碼後發現,SQLite In-Memory Database 不支援這個指令。

網路上有人建議用 DB::unprepared(file_get_contents("path/file.sql")) 手動載入,實測可行但速度反而更慢。

用 PDO::exec 直接載入 schema

關鍵是改用 PDO 的 exec 而不是 DB::unprepared

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
namespace Tests;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication;

    protected function setUpTraits()
    {
        // 必須在 parent::setUpTraits 之前
        $uses = array_flip(class_uses_recursive(static::class));
        $schema = database_path('schema/sqlite-schema.dump');
        if (isset($uses[RefreshDatabase::class]) &&
            $this->usingInMemoryDatabase() &&
            File::exists($schema)
        ) {
            DB::connection()->getPdo()->exec(File::get($schema));
        }

        parent::setUpTraits();
    }
}

實測從 2:21.979 秒降到 18.457 秒,快了 7 倍。

抽成可重用的 Trait

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace Tests\Traits;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;

trait RefreshInMemoryDatabase
{
    public function loadSchemaToInMemoryDatabase(): array
    {
        $uses = array_flip(class_uses_recursive(static::class));
        $schema = database_path('schema/sqlite-schema.dump');
        if (isset($uses[RefreshDatabase::class]) &&
            $this->usingInMemoryDatabase() &&
            File::exists($schema)
        ) {
            DB::connection()->getPdo()->exec(File::get($schema));
        }

        return $uses;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Tests\Traits\RefreshInMemoryDatabase;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication;
    use RefreshInMemoryDatabase;

    protected function setUpTraits()
    {
        // 必須在 parent::setUpTraits 之前
        $this->loadSchemaToInMemoryDatabase();

        parent::setUpTraits();
    }
}