Laravel × phpunit で実行速度を改善してみた

こちらは [mediba advent calendar 2016](http://qiita.com/advent-calendar/2016/mediba#day-25) の 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マイグレーションしている作りにしている。

```tests\TestCase.php
class TestCase extends Illuminate\Foundation\Testing\TestCase
{
	...省略
	
	public function setUp()
	{
	    parent::setUp();
	    Artisan::call('migrate:reset');
	    Artisan::call('migrate');
	}
}
    
```

フレームワークのTestCaseを見てみると以下のような作りになっている。

```Illuminate\Foundation\Testing\TestCase.php
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回だけ走るように改修しました。

```tests\TestCase.php
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回だけにしたらさらに早くなると思い同様にフラグを追加して実行してみます。

```tests\unit\service\AccountServiceTest.php
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倍のパフォーマンスを出すことができました。
他にもテストを実行するサーバのスペックを上げる、テスト自体を並行実行させる。など早くする方法は多々ありますが、身近でできることとしては**余計なことは実行しない**ということだと痛感しました。