Laravelの2種類のテストの比較(FeatureテストとUnitテスト)


はじめに

Laravelのtests/ディレクトリ以下にはFeature/Unit/の二つのディレクトリが自動で作成されます。それぞれのディレクトリにはExampleTest.phpが最初からありますが、これら二つの違いは「素のPHPUnitのTestCaseクラスを継承している」か「Laravel用に拡張されたTestCaseクラスを継承しているか」です。

Feature/ExampleTest.phpはLaravel用に拡張されたTestCaseクラスを継承しているので、DBアクセスを含むLaravelの機能がテスト内で全て使えます。Unit/ExampleTest.phpは素のPHPUnitのTestCaseクラスを継承しているため、Laravelの機能が使えず、素のPHPコードとしてのロジックテストを書くものになっています。

Featureテストは便利なのですが、Laravel用に拡張されている分テストの実行に時間がかかります。なので、Laravelに依存しない形で書けるテストは極力Unitテストにするべきです。

FeatureテストとUnitテストの比較

次のようなUser Modelがあります。isAdmin()メソッドでは、 roleカラムの値が「ADMIN」の場合にtrueを返します。このメソッドについて、FeatureテストとUnitテストを書いて比較します。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use HasFactory, Notifiable;

    protected $fillable = [
        'name',
        'email',
        'password',
        'role',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    public function isAdmin(): bool
    {
        return $this->role === 'ADMIN';
    }
}

Featureテストの場合

isAdmin()メソッドを呼び出すと、Userモデルは内部でDBアクセスを行い、roleカラムの値をDBから読み出そうとします。なので、Featureテストの場合、実際にDBへデータを登録してからisAdmin()メソッドを呼び出すことになります。

<?php

namespace Tests\Feature;

use App\Models\User;
use Tests\TestCase;

class UserTest extends TestCase
{
    public function test_isAdmin()
    {
        $user = User::factory()->create(['role' => 'ADMIN']);
        self::assertTrue($user->isAdmin());
    }
}

実行結果はこちらです。実行時間は[0.38s]でした。

sail test tests/Feature/UserTest.php 

   PASS  Tests\Feature\UserTest
  ✓ is admin

  Tests:  1 passed
  Time:   0.38s

Unitテストの場合

Unitテストの場合は、DBアクセスができません。では、どうするかというと、Userモデルをインスタンス化した後に、roleプロパティへ自分でデータをセットします。

<?php

namespace Tests\Unit;

use App\Models\User;
use PHPUnit\Framework\TestCase;

class UserTest extends TestCase
{
    public function test_isAdmin()
    {
        $user = new User();
        $user->role = 'ADMIN';

        // これでもOK
        // $user = new User(['role' => 'ADMIN']);

        self::assertTrue($user->isAdmin());
    }
}

実行結果はこちらです。実行時間は[0.06s]でした。Featureテストの1/6程度の時間で終わっています。

sail test tests/Unit/UserTest.php

   PASS  Tests\Unit\UserTest
  ✓ is admin

  Tests:  1 passed
  Time:   0.06s

まとめ

今回はFeatureテストとUnitテストの実行時間の比較を行いました。テスト対象のコードはとてもシンプルなものでしたが、それでも6倍以上の差でUnitテストの方が高速ということがわかりました。プロジェクトの全てのテストをFeatureテストにしてしまうと、少なくともこれだけ実行時間に差が出てしまうということですね。
また、実際のFeatureテストでは、裏でLaravelがよしなにやってくれていることも含めると、更に多くのLaravelの機能を使うことになると思います。そう考えるとFeatureテストとUnitテストでは6倍以上の実行時間の差が出るのではと思います。みなさん、Unitテスト書いていきましょう。そしてUnitテストが書けるような設計をしましょう。