速さこそ有能なのが、 文化の基本法則 (ストレイト・クーガー)
この記事について
Laravel Advent Calender 2019 17日目の記事です。
[https://qiita.com/advent-calendar/2019/laravel:embed:cite]
DBのテスト高速化: RefreshDatabaseトレイトのハック
対象読者
- DBが絡むテスト(以下DBテスト)を書いている
-
DBテストでインメモリDB (例: sqlite) を利用せず、本番環境同様のRDBMS (例: mysql) を利用している
- なるべく本番と揃えたいのが人情というものかと
- RefreshDatabaseトレイトを利用している
TL;DR
php artisan migrate:fresh
は遅いphp artisan migrate
を利用するようハックする
DBテストの開始には時間がかかりがち
DBテストは遅くなりがちです。
DBへの問い合わせを伴う処理自体が遅いのはもちろんのこと、テスト用テーブルの準備に時間がかかります。
テーブル数がそこそこあるオープンなプロジェクトをフォークし、何もしないテストを用意して計測してみました:
tests/SomeDBTest.php
<?php
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as IlluminateTestCase;
use Tests\Concerns\RefreshDatabaseLite;
class SomeDBTest extends IlluminateTestCase
{
use RefreshDatabaseLite, CreatesApplication;
/**
* @test
*/
public function something_is_something()
{
$this->assertTrue(true);
}
}
適当にphp実行環境を用意し、テストを実行してみます。
拙作:
[https://github.com/wand2016/docker-php:embed:cite]
Laravel Horizon依存なので、さらにext-pcntl
が必要です。
[f:id:wand_ta:20191215181411g:plain]
7秒ほどかかっているようです。
比較用に、DBが絡まない場合も:
<?php
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as IlluminateTestCase;
use Tests\Concerns\RefreshDatabaseLite;
class SomeTest extends IlluminateTestCase
{
use CreatesApplication;
/**
* @test
*/
public function something_is_something()
{
$this->assertTrue(true);
}
}
[f:id:wand_ta:20191215185121g:plain]
1.5秒くらい。差し引き5秒くらいの差が出ています。
理由 — migrate:fresh で毎回テーブルをdrop/createしている
先述のように、DBテストでは、テスト用テーブルの準備すなわちマイグレーションに結構時間がかかります。
[f:id:wand_ta:20191215181648g:plain]
テーブル数が増えてくると、マイグレーション待ちだけでトイレに行けるくらいになってきます。
そして、RefreshDatabase
トレイト使用時は、テストを実行するたびにmigrate:fresh
コマンドで全テーブルdrop後にマイグレーションが実行されます。
日々の開発で、特にTDDを実践して、しょっちゅうテストを回しているような場合、これでは辛くなります。
しかしDBテスト開始時にトランザクションを張り、終了時にロールバックしているなら、テーブルをdropして作り直す必要は滅多にありません。
そこで、migrate:fresh
の代わりにmigrate
を使用するようにします。
migrate
は、テーブルのdropを行いません。既にマイグレーション済のテーブルについては生成をスキップしてくれます。
[f:id:wand_ta:20191215185211g:plain]
migrate:freshではなくmigrateを使用するよう、RefreshDatabaseトレイトをハックする
RefreshDatabaseトレイトをハックし、 軽量版RefreshDatabaseLiteトレイトを作ります。
以下、Laravel v6の場合
tests/Concerns/RefreshDatabaseLite.php
<?php
declare(strict_types=1);
namespace Tests\Concerns;
use App\Console\Kernel;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\RefreshDatabaseState;
trait RefreshDatabaseLite
{
use RefreshDatabase;
/**
* Refresh a conventional test database.
*
* @return void
*/
protected function refreshTestDatabase()
{
if (! RefreshDatabaseState::$migrated) {
$this->artisan('migrate');
$this->app[Kernel::class]->setArtisan(null);
RefreshDatabaseState::$migrated = true;
}
$this->beginDatabaseTransaction();
}
}
RefreshDatabase
をuseしていたテストケースを修正し、RefreshDatabaseLite
をuseするようにします
<?php
namespace Tests;
- use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\TestCase as IlluminateTestCase;
+ use Tests\Concerns\RefreshDatabaseLite;
class SomeDBTest extends IlluminateTestCase
{
- use RefreshDatabase, CreatesApplication;
+ use RefreshDatabaseLite, CreatesApplication;
/**
* @test
*/
public function something_is_something()
{
$this->assertTrue(true);
}
}
[f:id:wand_ta:20191215182210g:plain]
速くなった!うれしい!
migrate:freshを実行するためのテストケースも用意する
マイグレーションファイルを追加していく分には、migrate
だけで十分です。
しかし、マイグレーションファイルに変更がある場合、migrate:fresh
が必要になってきます。
具体的には、
- 開発初期段階で、ALTERのmigrationを追加するのではなく、CREATEのmigrationを編集する
- 過去のバージョンのソースコードをチェックアウトする
といったケースです。
こういう場合のために、テスト用DBに対してmigrate:freshを実行するコマンド代わりのテストケースを作成します。
tests/MigrateFresh.php
<?php
declare(strict_types=1);
use Illuminate\Foundation\Testing\TestCase;
use Tests\CreatesApplication;
class MigrateFresh extends TestCase
{
use CreatesApplication;
/**
* @test
*/
public function migrate_fresh()
{
$this->artisan('migrate:fresh');
$this->assertTrue(true);
}
}
テストケースクラス名からはあえてTest
サフィックスを外してあります。
Test
サフィックスが付いていると、phpunitを引数なしで実行した際に毎回実施されてしまい、元も子もなくなってしまいます。(phpunit.xmlの設定で変えることもできます)
migrate:fresh
が必要になったら
./vendor/bin/phpunit tests/MigrateFresh.php
というように明示的にテストケースを指定して実行します。
[f:id:wand_ta:20191215191543g:plain]
適宜、シェルスクリプトなりcomposerスクリプトなりにすると良いと思います。
DB以外のテスト高速化: Docker Desktop for Windows から docker on WSL2へ移行する
もはやLaravel関係ありませんが、Laravelも速くなるのでね
WSL2については、最近発売された書籍「みんなのPHP 現場で役立つ最新ノウハウ!」でも取り上げられているようです。
[https://gihyo.jp/book/2019/978-4-297-11055-0:embed:cite]
みんな買おう!(#PR)
対象読者
- Docker Desktop for Windows勢
TL;DR
-
WSL2はいいぞ
- 「PHPだけ」のテストなら数倍速くなることも
- 他にもいろいろ嬉しい
- ネットワーク周りなど辛みもある
比較
導入については省きます
[https://wand-ta.hatenablog.com/entry/2019/09/09/215740:embed:cite]
ちょうどいいLaravelプロジェクトがなかったので、たまたま手元にあったRay.AOPで比較しました。
(最近Bear流行ってますね!)
Windows
ubuntu18.04 on WSL2
ハヤスギでしょwww
WSL2はいいぞ
Windows 10 ProでなくてもDockerを使える(らしい)
Docker Desktop for WindowsはHyper-V依存で、Hyper-VがWindows 10 Proでないと動作しません。
一方、WSL2はWindows 10 Homeでも動くらしいです。(試したことない)
WSL2の上ではLinuxが動き、Linuxの上ではDockerが動きます。
Docker Composeを使える
無印版WSLでDocker Composeを動かそうとすると、ネットワーク周りのエラーが出ていました。
WSL2ではそれが解決しています。
はやい
WSL2のrootfs以下に引きこもっている分には、上のスクショのように爆速です。
ただし、Windowsのファイルシステムのマウントポイント /mnt/
以下にアクセスすると台無しになるので注意。
メリットを享受するには、エディタもWSL2上に置いて、Windows側のX11サーバーでGUIをレンダリングする等、完全に引きこもる覚悟が必要です。
その他もろもろ
-
本物のlinuxを使える
- (マサカリ飛んできそう)
- systemdの使用等に制限はあります
- パーミッション破壊おじさんにならない
-
gitで
*.sh
をcheckoutしたとき「CRLF改行奴〜ww」にならない- 1行目が
#!/bin/sh^M
になってて、dockerコンテナが謎の死を遂げるやつ - .gitattributes書けって話なんですけどね
- 1行目が
WSL2のつらみ
つらみがあるのも確かです。
-
ネットワーク周り
- TLS over VPNがうまく繋がらなくてメソメソ泣いてます
-
/etc/resolv.conf
が常に調子悪い- 8.8.8.8とか追加しないとインターネットに出ていけない日がある
- 大丈夫な日もある。謎
-
ローカルループバックでハマりがち
127.0.0.1
だと繋がらなくて::1
だと繋がったりする
-
WSL2上にエディタを置くと、Windowsのデスクトップ上のファイルをエディタにドラッグ&ドロップできない
- だってWSL2にないファイルですから
人口が増えて頭のいい人が解決してくれると嬉しい…