LaravelでUnitテストを書くことを考えると、真っ先に選択肢に上がるのはPHPUnitかなと思います。
PHPUnitは標準でLaravelにインストールされており、導入のハードルも低いです。今回は、LaravelでPHPUnitを使うパターンと簡単な書き方についてまとめました。
LaravelでPHPUnitを使う基本的な方法
Laravelは5系以降なら標準でPHPUnitがインストールされています(4系以前は確認していません)。
下記のコマンドを実行してみるとPHPUnitの実行ができます。
./vendor/bin/phpunit
デフォルトでは、 PHPUnitの設定ファイルである ./phpunit.xml の設定が読み込まれ、./tests/Feature/ExampleTest.php と ./tests/Unit/ExampleTest.php の2つのテストファイルが実行されています。
それぞれ、どのような内容のテストが実行されているか確認してみると、
/** 省略 **/
class ExampleTest extends TestCase
{
/** ... */
public function testBasicTest()
{
$response = $this->get('/');
$response->assertStatus(200); // ここのテストが実行される
}
}
とステータスコードが200で返ってくるか?のテストが行われていることがわかります。
単体テストを書くのであれば、この testBasicTest()
のようなfunctionを条件分岐ごとに実装していけばいいことがわかります。
PHPUnitで特定のファイルを実行するには
単体テストの数が多くなってくると毎回全てのテストを実行していると非常に時間がかかるため、特定のテストのみを実行したいケースがあります。
そういったときは、特定のファイルを指定してあげることにより、そのファイルだけのテストを実行することができます。
./vendor/bin/phpunit ./tests/Unit/ExampleTest.php
また、パスを指定することすら面倒な場合は、ファイル名の指定のみで実行するテストを制御することも可能です。
./vendor/bin/phpunit --filter=ExampleTest
デフォルトの状態で上記コマンドを実行しても、それほど意味がありませんが、テスト数が増えてくると上記コマンドで実行するテスト数を減らすのも有効になります。
また2番目の書き方であればファイル単位ではなく、メソッド名単位でもテストを実行することができるため、より細かくUnitテストを実行することも可能です。
テストファイルの置き場所について
特に迷うことはないかもしれませんが、テストにあたってファイルは /app のnamespaceと同じように /tests に展開するようにファイルを置くと管理がしやすいかと思います。
例えば、/app/Http/Controllers/UserController.php のテストは、/tests/Http/Controllers/UserControllerTest.php に書くといった具合です。
namespaceをあわせておくとエディタでファイル検索をするのも楽なので、合わせておくといいですね。
データベースや外部APIをテストする場合
単体テストを書くときに迷うポイントとして、DBや外部APIに接続するテストを書くことだと思います。
なぜなら、接続先を外部のエンドポイントに繋ぐことを考えると、ローカル環境からでは
- 本番と同じデータを取得できない
- テスト用のエンドポイントを用意できない
といった問題が発生することが考えられるためです。これらの対策としては主に下記の2つの方法があると考えられます。
- 環境変数で接続先を切り替える
- mockを作成する
環境変数で接続先を切り替える
おそらく多くのプロジェクトではDBや外部APIのエンドポイントを.envファイルに記述しているでしょう。
その読み込む環境変数をPHPUnit実行時だけ別の環境変数にすることで、接続先を切り替えることができます。
Laravelの公式ドキュメントを見るとわかりますが、プロジェクトのルートで .env.testing ファイルを作ると .env ファイルの代わりに環境変数を取得してくれます。
したがって、
- .env には開発環境用のエンドポイント
- .env.testing にはテスト環境用のエンドポイント
を記述することで、開発環境とテスト環境を切り替えることができます。
mockを作成する
テストを書く人にとってはお馴染みですが、mockを作成するという方法もあります。
一応説明を加えておくと、mockとはテストで必要な値を擬似的に生成する機能を持つモジュールです。mockを使用することで、DBや外部APIに接続する値を擬似的に生成してテストを実行することができます。
例えば、下記のコードを見てみます。
/** 省略 **/
class UserRepository extends TestCase
{
/** ... */
public function testGetUser()
{
$this->assertInstanceOf(Collection::class, \UserModel::first()); // \User::first()はDB接続エラー
}
}
もしこのようなテストを書いたとしてもテスト用のDBを用意していない環境では、DBの接続エラーが発生します。
したがって、このようなケースでは、UserModelのmockを作成してあげることで擬似的に$user->first()
を実行できるようにしなければなりません。
具体的なmockの作り方
PHPUnitには、mockを作成できる phpunit-mock-objects というライブラリが組み込まれています。
もちろんこちらを使用してもいいですが、少々クセのあるライブラリなため標準ではMockeryというライブラリを使って実装するやり方がいいでしょう。
上記のテストでmockを作成することを考えると、下記のように実装できます。
/** 省略 **/
class UserRepository extends TestCase
{
/** ... */
private function createUserMock()
{
//Mockを設定
$mock = \Mockery::mock(UserModel::class);
$mock->shouldReceive('first')
->andReturn(new Collection());
return $mock;
}
/** ... */
public function testGetUser()
{
$user = $this->createUserMock();
$this->assertInstanceOf(Collection::class, $user); // テストが成功する
}
}
上記の例では、createUserMock でfirstというメソッドが実行された場合にCollectionクラスのインスタンスを返すmockを作成しています。
この例は、あまり良くないかもしれませんが、
ことによって、DBや外部APIから返ってくる値を擬似的に生成します。このようにmockを組み合わせることで、テストのcoverageも大きく改善が見込めます。
どのクラスにおいても80%以上のcoverageを目安にして実装をしていきたいところです。
まとめ
今回は、LaravelでPHPUnitを使用する方法と、その周辺の設計について大まかに解説しました。
テストは長期的にサービスを運用するにあたっては、バグを再発させないためにも必須かと思います。
最初は導入コストを考えると見合わないと感じるかもしれませんが、サービスを長く運営しているほどテストの価値を感じると思うのでぜひとも長期的目線でテストを書いていきましょう。