LaravelとPHPUnitでDB操作クラスのユニットテストを行う

公開
更新
注意最終更新から 7 年以上経過しています。最新の情報もあわせてご確認ください。

Laravel で作成したアプリケーションをより信頼のおけるものとするために、テストは有用です。

ユニットテストを行うにあたり、Laravel にはデフォルトで PHPUnit が使えるようになっています。

今回は、Laravel と PHPUnit を用いて、アプリケーションの中でデータベース操作が絡む部分のユニットテストを作成します。

開発環境

今回の開発環境は以下の通りです。

  • PHP 7.2
  • MySQL 8.0
  • Laravel 5.8
  • PHPUnit 7.5.9

Laravel のルートディレクトリを「laravel/」とします。

尚、今回はデータベース操作が絡む最小単位の機能テストを書く際の、DB やレコードの状態管理をメインとするので、TDD に則った手順ではなく、アサーションコード自体の細かい説明はあまり行いません。

テスト用 DB の用意

PHPUnit を使って自動テストを行う際にアプリケーション(開発)で使用している DB を用いる事も可能ですが、 実際に CRUD 操作を行うのでテスト用のスキーマを切って使用する事をおすすめします。

テスト実行時の DB 内容に左右されないようにテストコードを書く事で、より安定して自動テストを走らせることができます。

DB 作成

まずはテスト用の DB を用意します。 MySQL にログインして、テストに使用するスキーマを作成します。

mysql> create database test_laravel_db;

phpunit.xml

テスト用のスキーマを作成したら、それを phpunit.xml に記述します。

laravel/phpunit.xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="vendor/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
 <testsuites>
  <testsuite name="Unit">
   <directory suffix="Test.php">./tests/Unit</directory>
  </testsuite>

  <testsuite name="Feature">
   <directory suffix="Test.php">./tests/Feature</directory>
  </testsuite>
 </testsuites>
 <filter>
  <whitelist processUncoveredFilesFromWhitelist="true">
   <directory suffix=".php">./app</directory>
  </whitelist>
 </filter>
 <php>
  <server name="APP_ENV" value="testing"/>
  <server name="BCRYPT_ROUNDS" value="4"/>
  <server name="CACHE_DRIVER" value="array"/>
  <server name="MAIL_DRIVER" value="array"/>
  <server name="QUEUE_CONNECTION" value="sync"/>
  <server name="SESSION_DRIVER" value="array"/>
  // 追加 ↓
  <env name="DB_DATABASE" value="test_laravel_db"/>
 </php>
</phpunit>

これでテストを実行した際にはテスト用の DB が使用されるようになりました。

.env.testing

テスト実行時は env ファイルの環境(APP_ENV)が testing に切り替わります。 その時に.env.testing が存在していればそちらの環境変数を見に行くので、テスト用 env ファイルを作成しておくと本番とは違う環境変数値を使いたい場合に便利です。

ユニットテスト

ここからユニットテストを書いていきます。

インターフェイスのユニットテスト

まずはテストファイルを生成します。今回は Repository パターンでデータアクセス部分を定義したのでインターフェースのテストコード記述します。

以下の artisan コマンドを叩いて、ユニットテス用のクラスを生成します。

# Laravel ルートディレクトリへ移動
cd /path/to/laravel

# Unit ディレクトリにテストを生成する
php artisan make:test MembersRepositoryInterfaceTest --unit

# 実行結果
[demo@localhost laravel]$ php artisan make:test MembersRepositoryInterfaceTest --unit
Test created successfully.

laravel/tests/Unit 配下に MembersRepositoryInterfaceTest.php が生成されます。

laravel/
    ├─ tests
        ├─ Unit
            └─ MembersRepositoryInterfaceTest.php

これを、定義されたメソッドについてテストコードを定義します。

laravel/tests/Unit/MembersRepositoryInterfaceTest.php
<?php
declare(strict_types=1);

namespace Tests\Unit;

use Tests\TestCase;

abstract class MembersRepositoryInterfaceTest extends TestCase
{
    /**
     * @test
     * @group DataAccess
     * @group Members
     * @group all
     */
    public function all_返り値は配列であること()
    {
        $this->assertTrue(is_array($this->Members->all()));
    }

    /**
     * @test
     * @group DataAccess
     * @group Members
     * @group all
     */
    public function all_必要なフィールドが取得されている事()
    {
        $data = $this->Members->all();

        $expected = [
            'id', 'name', 'created_at', 'updated_at'
        ];

        $this->assertSame($expected, array_keys($data[0]));
    }

    /**
     * @test
     * @group DataAccess
     * @group Members
     * @group get
     */
    public function get_返り値は配列であること()
    {
        $this->assertTrue(is_array($this->Members->get(1)));
    }

    /**
     * @test
     * @group DataAccess
     * @group Members
     * @group get
     */
    public function get_必要なフィールドが取得されている事()
    {
        $data = $this->Members->get(1);

        $expected = [
            'id', 'name', 'created_at', 'updated_at'
        ];

        $this->assertSame($expected, array_keys($data));
    }

    /**
     * @test
     * @group DataAccess
     * @group Members
     * @group insert
     */
    public function insert_登録処理が成功する事を検証()
    {
        $name = 'member_011';

        $this->Members->insert($name);

        $this->assertDatabaseHas('members', [
            'name' => $name
        ]);
    }

    /**
     * @test
     * @expectedException \Exception
     * @group DataAccess
     * @group Members
     * @group insert
     */
    public function insert_空文字が渡ってきた場合には例外を投げる事()
    {
        $this->Members->insert('');
    }

    /**
     * @test
     * @group DataAccess
     * @group Members
     * @group update
     */
    public function update_更新処理が成功する事を検証()
    {
        $id = 1;
        $name = 'MEMBER_1';

        $this->Members->update($id, $name);

        $this->assertDatabaseHas('members', [
            'id' => $id,
            'name' => $name
        ]);
    }

    /**
     * @test
     * @expectedException \Exception
     * @group DataAccess
     * @group Members
     * @group update
     */
    public function update_空文字が渡ってきた場合には例外を投げる事()
    {
        $this->Members->update(1, '');
    }

    /**
     * @test
     * @group DataAccess
     * @group Members
     * @group delete
     */
    public function delete_削除処理が成功する事を検証()
    {
        $id = 1;

        $this->Members->delete($id);

        $this->assertDatabaseMissing('members', [
            'id' => $id
        ]);
    }
}
  • インターフェースのテストなので、抽象クラス(abstract)として定義しています。
  • インターフェイスで宣言したメソッドについて、テストを定義しています。

この時点ではまだデータベースの準備に関する事は何も定義していません。
インターフェイス自体はそれ単体で動作するものではないので、テストクラスにはメソッドの規則としてのテストコードのみを記述しておきます。

尚、今回のサンプルアプリケーションには単純なメソッド(処理)のみが宣言・実装されているのでテストコードもシンプルなものになっています。 各々のアプリケーションにテストを定義する場合はもちろん、要件に合ったものを定義します。

派生クラスのユニットテスト

次に派生クラスのテストコード書いていきます。 以下の artisan コマンドを叩いて、ユニットテスト用のクラスを生成します。

# Laravel ルートディレクトリへ移動
cd /path/to/laravel

# Unit ディレクトリにテストを生成する
php artisan make:test MembersEloquentRepositoryTest --unit

# 実行結果
[demo@localhost laravel]$ php artisan make:test MembersEloquentRepositoryTest --unit
Test created successfully.

laravel/tests/Unit 配下に MembersEloquentRepositoryTest.php が生成されます。

laravel/
    ├─ tests
        ├─ Unit
            ├─ MembersEloquentRepositoryTest.php
            └─ MembersRepositoryInterfaceTest.php

これからテストを定義していきますが、サンプルアプリケーションを見るとわかるとおり、 このクラスはインターフェイスのメソッドを実装しているのみで、独自のメソッドは定義していません。 インターフェイスを実装したコードはインターフェイスのテストで評価されるので、ここでは記述する必要はありません。

ここでは、データベースを使用してテストを行うその前準備と後始末を定義します。

laravel/tests/Unit/MembersEloquentRepositoryTest.php
<?php
declare(strict_types=1);

namespace Tests\Unit;

use Illuminate\Support\Facades\Artisan;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Repositories\MembersEloquentRepository AS Members;

class MembersEloquentRepositoryTest extends MembersRepositoryInterfaceTest
{
    use RefreshDatabase;

    protected $Members;

    public function setUp(): void
    {
        parent::setUp();

        $this->seed('MembersTableSeeder');

        $this->Members = new Members();
    }

    public function tearDown(): void
    {
        Artisan::call('migrate:refresh');
        parent::tearDown();
    }
}

上から解説します。

class MembersEloquentRepositoryTest extends MembersRepositoryInterfaceTest

インターフェイスのテストクラスを継承しています。 こうすることによって、派生クラスのテストを実行時にインターフェイス側で定義したテストも実行されます。

use Illuminate\Foundation\Testing\RefreshDatabase;

class MembersEloquentRepositoryTest extends MembersRepositoryInterfaceTest
{
    use RefreshDatabase;
}

RefreshDatabase トレイトを使用しています。 テスト開始時に1度だけマイグレーションを実行し、テーブルを作成してくれます。 (データの投入は行いません。)

public function setUp(): void
{
    parent::setUp();

    $this->seed('MembersTableSeeder');

    $this->Members = new Members();
}

setUp メソッドを定義しています。
これは、このテストクラスの各メソッドの開始前に必ず呼ばれるメソッドです。
つまり、メソッドの数だけコールされます。

  1. 親クラスの setUp メソッドを実行(必須)
  2. Members テーブルのシーディングを seed() メソッドで実行
  3. MembersRepository をインスタンス化

ここで、Members テーブルにシーダ経由でデータを投入しています。

public function tearDown(): void
{
    Artisan::call('migrate:refresh');
    parent::tearDown();
}

tearDown メソッドを定義しています。
これは、このテストクラスの各メソッドの終了後に必ず呼ばれるメソッドです。
つまり、メソッドの数だけコールされます。

  1. Artisan ファサードの call メソッドを用いて、全ロールバックからのマイグレーション実行させ、 データベースを初期状態(テーブル再構築&データ無し)に戻しています。
  2. 親クラスの tearDown メソッドを実行(必須)

テスト実行

これで一通り定義したので、一度実行してみます。

# Laravel ルートディレクトリへ移動
cd /path/to/laravel

# テスト実行
phpunit

# 実行結果
[demo@localhost laravel]$ phpunit

==== Redirecting to composer installed version in vendor/phpunit ====

PHPUnit 7.5.9 by Sebastian Bergmann and contributors.

..........                                                        10 / 10 (100%)

Time: 8.11 seconds, Memory: 24.00 MB
OK (10 tests, 10 assertions)

テストが実行され、全てが通る事が確認できました。

毎回実行する DB の初期化について

例えば今回のように、setUp() でシーディングを行い、tearDown() で DB を初期化する。 これを毎回のメソッド毎に行われる事がとても無駄であると感じる事もあります。 実際、もしこれが SELECT のテストのみであった場合、DB 内のレコードは何も変更しないので、毎回作り直す必要はありません。

ただしテストは同じクラスであっても、その1つ1つが独立している必要があり、特定の場合を除いてはどこかのテストがどこかのテストの影響を受けるべきではありません。 その考えと同じで DB も、どこかのテストがレコードに与えた変更で別のテストが失敗してしまうような構造は絶対に避けるべき点です。 それを考えると、1回1回しっかりと初期化していくというのはテストを書く自由度やモチベーションを失わない為に必要だと思います。

任意のデータを挿入する

これまでは、シーディングを実行してまとめてレコードを挿入していました。 ただ場合によっては任意のレコードを任意のタイミングで、任意の数挿入しテストを行いたい場合もあります。

ここでは、シーディングではなく個別にテスト DB にレコードを挿入してみます。

Factory クラス

まずは Factory クラスを作成します。 以下の artisan コマンドを叩いて、Factory クラスを生成します。

# Laravel ルートディレクトリへ移動
cd /path/to/laravel

# Factory クラス生成
php artisan make:factory MemberFactory

# 実行結果
[demo@localhost laravel]$ php artisan make:factory MemberFactory
Factory created successfully.

laravel/database/factories 配下に MemberFactory.php が生成されます。

laravel/
    ├─ database
        ├─ factories
            └─ MemberFactory.php

これを例えば、以下のように定義します。

laravel/database/factories/MemberFactory.php
<?php

/* @var $factory \Illuminate\Database\Eloquent\Factory */

use App\Member;

$factory->define(Member::class, function () {
    return [
        'name' => 'member_sub_001'
    ];
}, 'member_sub_1');


$factory->define(Member::class, function () {
    return [
        'name' => 'member_sub_002'
    ];
}, 'member_sub_2');


$factory->define(Member::class, function () {
    return [
        'name' => 'member_sub_003'
    ];
}, 'member_sub_3');

全部で3つのレコードを定義しています。こうする事で、テストの中で任意のタイミングでこれらを挿入できます。 実際に使用してみます。

laravel/tests/Unit/MembersEloquentRepositoryTest.php
public function setUp(): void
{
    parent::setUp();

    // $this->seed('MembersTableSeeder');
    // ↓
    factory(\App\Member::class, 'member_sub_1')->create();
    factory(\App\Member::class, 'member_sub_3')->create();

    $this->Members = new Members();
}

ここでは、setUp メソッドの中で2つのレコードを挿入しています。

もちろん setUp() の中で使わなくても良くて、各メソッドの中でこうしてレコードを挿入すれば、 そのテストに応じて必要最小限のレコードを用意すれば良くなり、パフォーマンス向上に繋がります。

Faker によるダミーデータの挿入

さっきの任意のレコードを挿入する場合もこれに含まれますが、シーダに定義するレコードというのは、アプリケーションをゼロから動かす場合に必要不可欠な値を予め定義しておく事がほとんどだと思います。

例えば、記事やメッセージを扱うテーブルがあった場合に、それらのレコードをシーダに予め定義しておく事はしませんよね。 なのでそういった性質のデータを扱う機能をテストしようとした時には、シーディングではない別の手段でテストデータを挿入してあげる必要があります。

それにはもちろん、先に紹介した任意のレコードを定義して挿入する方法が有用です。 しかし、データの定義がとても面倒であり、複数のレコードを全て定義していくのはとても骨が折れるし現実的ではありません。 その場合には、ダミーデータを挿入してテストを行いましょう。

Laravel には Faker というダミーデータを生成するパッケージが導入されていて、テストでも気軽に使用できます。

先に作成した、Factory クラスを以下に変更します。

laravel/database/factories/MemberFactory.php
// Fakerによるレコード定義
$factory->define(Member::class, function (Faker $faker) {
    return [
        'name' => $faker->name
    ];
});

Faker がどんなダミーデータを生成できるのかは、オフィシャル を見ると確認できます。

ではこれをテストクラスで使用します。

laravel/tests/Unit/MembersEloquentRepositoryTest.php
public function setUp(): void
{
    parent::setUp();

    // $this->seed('MembersTableSeeder');
    // ↓
    // factory(\App\Member::class, 'member_sub_1')->create();
    // factory(\App\Member::class, 'member_sub_3')->create();
    // ↓
    factory(\App\Member::class, 10)->create();

    $this->Members = new Members();
}

factory メソッドの第二引数で、何個のレコードを挿入するかを指定しています。ここでは 10 件のダミーデータを挿入しています。

ちなみに 5 件生成した場合はこんな感じに生成されました。

Array
(
    [0] => Array
        (
            [id] => 1
            [name] => Mr. Arnaldo Buckridge
        )

    [1] => Array
        (
            [id] => 2
            [name] => Mrs. Maida Schaden DVM
        )

    [2] => Array
        (
            [id] => 3
            [name] => Ayana Aufderhar
        )

    [3] => Array
        (
            [id] => 4
            [name] => Dr. Stephan Raynor
        )

    [4] => Array
        (
            [id] => 5
            [name] => Dr. Ethan O'Conner
        )
)

このように、幾つでも必要なだけデータを挿入できて、テスト時にも重宝します。

ちなみにダミーデータを日本語にしたい場合はFaker で生成するダミーデータを日本語化する で方法を確認できます。

まとめ

以上で作業は完了です。 データベースが絡む部分のテストは結構大切で、ここをしっかり抑えておけば、あとのビジネスロジックや細かい部分のテストのデータ部分は全部モック化してしまえる 安心感が得られるし、何よりそれで自動テストの実行時間も大幅に短縮されるはずです。 是非試してみてください。

Ritolabo