こちらは mediba advent calendar 2016 の 25日目の記事です。
はじめに
私の所属しているチームはLaravelを用いて開発しており、Laravel標準のPHPUnitがとても遅いのでなんとかしたいと思いこの記事を書きました。
開発環境は少し古いバージョンを使っていますが、以下の通り
- CentoOS 6.7
- Laravel 5.2
- php 5.6.29
- phpunit 4.8.27
実現したいこと
1ファイルのテスト実行した結果がこちら。
[vagrant@local current]$ vendor/bin/phpunit tests/unit/service/AccountServiceTest.php
PHPUnit 4.8.27 by Sebastian Bergmann and contributors.
..................
Time: 26.2 seconds, Memory: 26.00MB
OK (18 tests, 28 assertions)
...とても遅い。
1ファイル18テスト28アサーションを実行するのに26秒近く掛かっているので、もっと早くなるようにする。
前提
今回テストで使うディレクトリ構成は以下の様になっています
.
├── app
├── artisan
├── bootstrap
├── composer.lock
├── config
├── database
│ ├── factories
│ ├── migrations
│ └── seeds
│ ├── AccountSeeder.php
│ └── tests
│ └── TestAccountSeeder.php
├── frontdocs
├── phpunit.xml
├── public
├── resources
├── server.php
├── storage
├── tests
│ ├── TestCase.php
│ └── unit
│ └── service
│ └── AccountServiceTest.php
└── vendor
テスト用のSeedは最低限のものだけにしたかったので、通常のものと分けています。
やったこと
テストを実行したタイミングでデータベースを見てみると毎回テーブルを作成してデータを入れては消すを繰り返しているので、これが原因らしいのでコードを直してみる。
まずベースとなるTestCase.php
はlaravel標準のIlluminate\Foundation\Testing\TestCase
をextendsして呼んでおり、setUpでDBマイグレーションしている作りにしている。
class TestCase extends Illuminate\Foundation\Testing\TestCase
{
...省略
public function setUp()
{
parent::setUp();
Artisan::call('migrate:reset');
Artisan::call('migrate');
}
}
フレームワークのTestCaseを見てみると以下のような作りになっている。
abstract class TestCase extends PHPUnit_Framework_TestCase
{
...省略
protected function setUp()
{
if (! $this->app) {
$this->refreshApplication();
}
$this->setUpTraits(); // ここで毎回データベースをセットし直しているのが遅い原因
foreach ($this->afterApplicationCreatedCallbacks as $callback) {
call_user_func($callback);
}
$this->setUpHasRun = true;
}
}
ベースのIlluminate\Foundation\Testing\TestCase
を継承するとsetUpTraits
で db migrateを毎回するので、parent::setUp
しないほうが良さそうです。
また、最初だけ実行するようなフラグを用意してマイグレーションは1回だけ走るように改修しました。
class TestCase extends Illuminate\Foundation\Testing\TestCase
{
static private $databaseSetup = false;
...省略
public function setUp()
{
if (! $this->app) {
$this->refreshApplication();
}
// 最初のみ実行するフラグを追加
if (!static::$databaseSetup) {
Artisan::call('migrate:reset');
Artisan::call('migrate');
static::$databaseSetup = true;
}
}
}
これで早くなるはずです。
実行結果
[vagrant@local current]$ vendor/bin/phpunit tests/unit/service/AccountServiceTest.php
PHPUnit 4.8.27 by Sebastian Bergmann and contributors.
..................
Time: 17.24 seconds, Memory: 24.25MB
OK (18 tests, 28 assertions)
26秒から17秒になり、9秒短縮!!
これで目的は達成しているが、マイグレーションだけでなくシードもテストが実行されるたびデータを入れているので最初の1回だけにしたらさらに早くなると思い同様にフラグを追加して実行してみます。
class AccountServiceTest extends TestCase
{
use DatabaseMigrations;
static private $databaseSeed = false;
protected $account_model;
public function setUp()
{
parent::setUp();
$this->account_model = new Account();
// 最初のみ実行するフラグを追加
if (!static::$databaseSeed) {
Artisan::call('db:seed', [
'--class' => 'tests\\TestAccountSeeder'
]);
static::$databaseSeed = true;
}
}
...省略
}
実行結果
[vagrant@local current]$ vendor/bin/phpunit tests/unit/service/AccountServiceTest.php
PHPUnit 4.8.27 by Sebastian Bergmann and contributors.
..................
Time: 5.40 seconds, Memory: 21.75MB
OK (18 tests, 28 assertions)
実行結果はなんと5秒!!
最初の26秒と比較すると21秒も短縮することができました。
まとめ
テストケースごとに呼ばれるsetUpメソッドで余計なことをしないシンプルな作りにするだけで約5倍のパフォーマンスを出すことができました。
他にもテストを実行するサーバのスペックを上げる、テスト自体を並行実行させる。など早くする方法は多々ありますが、身近でできることとしては余計なことは実行しないということだと痛感しました。