RitoLabo

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

  • 公開:
  • 更新:
  • カテゴリ: PHP Laravel
  • タグ: Laravel,Faker,Factory,Interface,Repository,PHPUnit

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

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

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

アジェンダ
  1. 開発環境
  2. テスト用DBの用意
    1. DB作成
    2. phpunit.xml
    3. .env.testing
  3. ユニットテスト
    1. インターフェイスのユニットテスト
    2. 派生クラスのユニットテスト
  4. テスト実行
  5. 毎回実行するDBの初期化について
  6. 任意のデータを挿入する
    1. Factoryクラス
    2. Fakerによるダミーデータの挿入

開発環境

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

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

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

今回テストコードを書くにあたり、サンプルのアプリケーションを作成しています。 同じ流れで試してみたい場合は以下より作成するか、Githubからソースをダウンロードしてください。

尚、今回はデータベース操作が絡む最小単位の機能テストを書く際の、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で生成するダミーデータを日本語化するで方法を確認できます。

まとめ

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

今回のソースコードはGithubにアップしてあります。