Laravelのテストを高速化した話 (Laravelアドベントカレンダー20191217)
速さこそ有能なのが、 文化の基本法則 (ストレイト・クーガー)
- この記事について
- DBのテスト高速化: RefreshDatabaseトレイトのハック
- DB以外のテスト高速化: Docker Desktop for Windows から docker on WSL2へ移行する
この記事について
Laravel Advent Calender 2019 17日目の記事です。
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実行環境を用意し、テストを実行してみます。
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); } }
1.5秒くらい。差し引き5秒くらいの差が出ています。
理由 -- migrate:fresh で毎回テーブルをdrop/createしている
先述のように、DBテストでは、テスト用テーブルの準備すなわちマイグレーションに結構時間がかかります。
テーブル数が増えてくると、マイグレーション待ちだけでトイレに行けるくらいになってきます。
そして、RefreshDatabase
トレイト使用時は、テストを実行するたびにmigrate:fresh
コマンドで全テーブルdrop後にマイグレーションが実行されます。
日々の開発で、特にTDDを実践して、しょっちゅうテストを回しているような場合、これでは辛くなります。
しかしDBテスト開始時にトランザクションを張り、終了時にロールバックしているなら、テーブルをdropして作り直す必要は滅多にありません。
そこで、migrate:fresh
の代わりにmigrate
を使用するようにします。
migrate
は、テーブルのdropを行いません。既にマイグレーション済のテーブルについては生成をスキップしてくれます。
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); } }
速くなった!うれしい!
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
というように明示的にテストケースを指定して実行します。
適宜、シェルスクリプトなりcomposerスクリプトなりにすると良いと思います。
DB以外のテスト高速化: Docker Desktop for Windows から docker on WSL2へ移行する
もはやLaravel関係ありませんが、Laravelも速くなるのでね
WSL2については、最近発売された書籍「みんなのPHP 現場で役立つ最新ノウハウ!」でも取り上げられているようです。
みんな買おう!(#PR)
対象読者
- Docker Desktop for Windows勢
TL;DR
- WSL2はいいぞ
- 「PHPだけ」のテストなら数倍速くなることも
- 他にもいろいろ嬉しい
- ネットワーク周りなど辛みもある
比較
導入については省きます
ちょうどいいLaravelプロジェクトがなかったので、たまたま手元にあったRay.AOPで比較しました。
(最近Bear流行ってますね!)
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のつらみ
つらみがあるのも確かです。
人口が増えて頭のいい人が解決してくれると嬉しい…