【輪読会資料】PHPフレームワーク Laravel Webアプリケーション開発 9章テスト 9-1『ユニットテスト』
以下の記事は2019年1月10日、コワーキングスペース秋葉原Weeybleにて行われる [秋葉原] Laravel Webアプリケーション開発 輪読会 (9章 テスト)の輪読会資料の一部となります。 今回は 9-1 章部分の『ユニットテスト』部分の記事をアップします。
また、元になっている書籍は以下となります。

PHPフレームワーク Laravel Webアプリケーション開発 バージョン5.5 LTS対応
- 作者: 竹澤有貴,栗生和明,新原雅司,大村創太郎,丸山弘詩
- 出版社/メーカー: ソシム
- 発売日: 2018/09/26
- メディア: 単行本
- この商品を含むブログを見る
PHPフレームワーク Laravel Webアプリケーション開発 輪読会資料
Chapter 9 テスト
テストコード実装の基礎と実践
目次
- 9-1 ユニットテスト
- 9-1-1 テスト対象クラス
- 9-1-2 テストクラスの生成
- 9-1-3 テストメソッドの実装
- 9-1-4 データプロバイダの活用
- 9-1-5 例外のテスト
- 9-1-6 テストの前処理・後処理
- 9-1-7 テストの設定
- 9-2 データベーステスト
- 9-2-1 テスト対象のテーブルとクラス
- 9-2-2 データベーステストの基礎
- 9-2-3 Eloquentクラスのテスト
- 9-2-4 サービスクラスのテスト
- 9-2-5 モックによるテスト(サービスクラス)
- 9-3 WebAPIテスト
なお、説明中のサンプルコードは以下のリンクより取得したものです。
PHPフレームワーク Laravel Webアプリケーション開発 laravel-socym/chapter09 by GitHub
9-1 ユニットテスト
Lravelでサポートされるテスト機能
余談
いつも見るサンプルコードの冒頭の奴declare(strict_types=1);
は、
厳密な型チェックモードに設定している。
これを記述することによって型指定をして、間違った値が入るとエラーを出すそうです。
9-1-1 テスト対象クラス
ここではポイント算出のメソッドを元にtestを行う
app\Services\CalculatePointService.php
<?php declare(strict_types=1); namespace App\Services; use App\Exceptions\PreConditionException; final class CalculatePointService { /** * @param int $amount * @return int * @throws PreConditionException */ public static function calcPoint(int $amount): int { if ($amount < 0) { throw new PreConditionException('購入金額が負の数'); } if ($amount < 1000) { return 0; } if ($amount < 10000) { $basePoint = 1; } else { $basePoint = 2; } return intval($amount / 100) * $basePoint; } }
要約すると以下の処理をするメソッド
買い物をした際に付く、〇〇ポイントの算出をする機能。以下のルールで行う
購入金額 | ポイント |
---|---|
0~999 | ポイント無し |
1,000~9,999 | 100円につき1ポイント |
10,000以上 | 100円につき2ポイント |
9-1-2 テストクラスの生成
ユニットテストを記述するテストクラスは以下のコマンドで生成する
$ php artisan make:test CalculatePointServiceTest --unit Test created successfully.
以下のパスにファイルが生成される
tests\Unit\CalculatePointServiceTest.php
9.1.2.4 生成されたCalculatePointServiceTestクラス
<?php namespace Tests\Unit; use Tests\TestCase; use Illuminate\Foundation\Testing\WithFaker; use Illuminate\Foundation\Testing\RefreshDatabase; class CalculatePointServiceTest extends TestCase { /** * A basic test example. * * @return void */ public function testExample() { $this->assertTrue(true); } }
ここで大事な補足
書籍にはないが、 Testクラスを作成したらテスト対象となるクラスを use させないと駄目。
今回のテスト対象はApp\Services\CalculatePointService
なので以下の一行をTestファイルのクラス宣言前に追加する。
<?php namespace Tests\Unit; use Tests\TestCase; use Illuminate\Foundation\Testing\WithFaker; use Illuminate\Foundation\Testing\RefreshDatabase; // この行を追加 use App\Services\CalculatePointService; class CalculatePointServiceTest extends TestCase { // 省略 }
リスト9.1.2.2 testsディレクトリの構成
tests ├─Feature // フィーチャ機能テストのディレクトリ │ └─ExampleTest.php ├─Unit // ユニットテストのディレクトリ │ ├─CalculatePointServiceTest.php // 生成されたテストクラス(これ以外はデフォルト) │ └─ExampleTest.php ├──CreatesApplication.php └──TestCase.php // テスト基底クラス
テストクラスを実装する際は Tests\TestCase クラスを継承することが多いが、フレームワークの機能を使わない場合は PHPUnit\Framework\TestCase クラスを直接敬称しても問題ない。
9.1.2.3 テストクラスのクラス図
生成されたtestClassには以下の様な test... で始まるメソッドが作られる
test... から始まるメソッドがテストメソッドとして実行される。
9.1.2.4 抜粋 生成された CalculatePointServiceTest クラス
<?php public function testExample() { $this->assertTrue(true); }
別の方法でコメントに @test アノテーションを付ける方法もある。作者はこっちをお勧めしてる
参考: アノテーションについて
9.1.2.5 @testアノテーション
<?php /** * @test */ public function Example() { $this->assertTrue(true); }
さらにアノテーションと、メソッド名を日本語にすると(え!?) test結果の判別が付けやすくなる
<?php /** * @test */ public function divide_除数がゼロなら例外を投げる() { // (略) }
9.1.2.6 テストの実行例
phpunitコマンドに続けてテストクラスファイルを指定する
$ ./vendor/bin/phpunit tests/Unit/CalculatePointServiceTest.php PHPUnit 6.5.9 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 1.94 seconds, Memory: 10.00MB OK (1 test, 1 assertion)
9.1.2.8 テストの失敗例
vagrant@homestead:~/larabook/chapter09$ ./vendor/bin/phpunit tests/Unit/CalculatePointServiceTest.php PHPUnit 6.5.9 by Sebastian Bergmann and contributors. F 1 / 1 (100%) Time: 1.04 seconds, Memory: 10.00MB There was 1 failure: 1) Tests\Unit\CalculatePointServiceTest::Example Failed asserting that false is true. /home/vagrant/larabook/chapter09/tests/Unit/CalculatePointServiceTest.php:19 FAILURES! Tests: 1, Assertions: 1, Failures: 1.
コマンドの際 phpunit コマンドの引数を省略すると 全テストが実行される。よく使うので覚えておくと良い
9.1.2.9 全てのテストの実行例
$ ./vendor/bin/phpunit
但し、アプリケーションの規模が大きい場合は、テスト実行時間が恐ろしい事になるので安易やらない方が良い。(Ruby on Railsでの経験則)
9-1-3 テストメソッドの実装
ポイント付与のサンプルコードを元にテストメソッドを実装する
ここでは、ポイント算出ルールの境界値に沿ってテストを記述する
まず、購入金額が0円のパターンを検査する
0円の場合ポイントが0になるか?
9.1.3.2 主なアサーションメソッド(抜粋)
メソッド | 内容 |
---|---|
assertSame | 型も含めて期待値と値が一致するかを検証 |
assertTrue | 値がtrueかどうかを検証 |
assertReqExp | 値が正規表現にマッチするかどうかを検証 |
assertArrayHasKey | 値が配列の場合、指定したキーが存在するかを検証 |
tests\Unit\CalculatePointServiceTest.php
に以下のメソッドを追加
<?php /** * @test */ public function calcPoint_購入金額が0ならポイントは0() { $result = CalculatePointService::calcPoint(0); $this->assertSame(0, $result); // $result が0である事を検証 }
テストを実行すると2つのtestが通る事を確認できる
vagrant@homestead:~/larabook/chapter09$ ./vendor/bin/phpunit tests/Unit/CalculatePointServiceTest PHPUnit 6.5.9 by Sebastian Bergmann and contributors. .. 2 / 2 (100%) Time: 4.43 seconds, Memory: 10.00MB OK (2 tests, 2 assertions)
同様に今度は購入金額1000円でのテストを書く
<?php /** * @test */ public function calcPoint_購入金額が1000ならポイントは10() { $result = CalculatePointService::calcPoint(1000); $this->assertSame(10, $result); // $result が10である事を検証 }
同様にテストが3つ通る事を検証
vagrant@homestead:~/larabook/chapter09$ ./vendor/bin/phpunit tests/Unit/CalculatePointServiceTest PHPUnit 6.5.9 by Sebastian Bergmann and contributors. ... 3 / 3 (100%) Time: 1.03 seconds, Memory: 10.00MB OK (3 tests, 3 assertions)
こんな感じでテスト項目を増やすって事らしい
9-1-4 データプロバイダの活用
テストメソッドは引数と戻り値の組み合わせのみだが、データプロバイダは同じ処理に対して、異なるパラメータや引数を渡してテストする事ができて便利。
テストメソッドに渡すパラメータを指定するメソッドを用意する
データプロバイダメソッドは public
にする必要がある
9.1.4.1 データプロバイダメソッドの例
<?php public function dataProvider_for_calcPoint(): array { return [ '購入金額が0なら0ポイント' => [0, 0], '購入金額が999なら0ポイント' => [0, 999], '購入金額が1000なら10ポイント' => [10, 1000], ]; }
データプロバイダを利用するには、テストメソッドに @dataProvider
アノテーションを指定する。
9.1.4.2 データプロバイダを利用したテストメソッド
<?php /** * @test * @dataProvider dataProvider_for_calcPoint */ public function calcPoint(int $expected, int $amount) { $result = CalculatePointService::calcPoint($amount); $this->assertSame($expected, $result); }
仮に以下の値をあえて間違った上でテストを実行してみる
'購入金額が1000なら10ポイント' => [0, 1000],
vagrant@homestead:~/larabook/chapter09$ ./vendor/bin/phpunit tests/Unit/CalculatePointServiceTest PHPUnit 6.5.9 by Sebastian Bergmann and contributors. .....F 6 / 6 (100%) Time: 1.34 seconds, Memory: 10.00MB There was 1 failure: 1) Tests\Unit\CalculatePointServiceTest::calcPoint with data set "購入金額が1000なら10ポイント" (0, 1000) Failed asserting that 10 is identical to 0. /home/vagrant/larabook/chapter09/tests/Unit/CalculatePointServiceTest.php:59 FAILURES! Tests: 6, Assertions: 6, Failures: 1.
1エラーとなりエラーとなった配列のキー(日本語)と、配列の組み合わせ(0,1000)が出力されるので、エラー詳細が明快になる。
より多くのテストパターンを実装、数値の境界部分の値を追加している
9.1.4.6 データプロバイダメソッドに要素を追加
<?php public function dataProvider_for_calcPoint() : array { return [ '購入金額が0なら0ポイント' => [0, 0], '購入金額が999なら0ポイント' => [0, 999], '購入金額が1000なら10ポイント' => [10, 1000], '購入金額が9999なら99ポイント' => [99, 9999], '購入金額が10000なら200ポイント' => [200, 10000], ]; }
こんな感じでデータプロバイダメソッドでは複数の値、パラメータでテストが行える。
9-1-5 例外のテスト
throw で投げられた例外処理をテストするには、以下の利用方法がある
- try/catch の利用
- expectException メソッド
- @expectedExcepsion アノテーション
作者が薦めるのは3つめのアノテーション、ここでは
- 例外がスローされるか?
- スローされた例外が意図したものであるか?
を検証する
言葉の意味は expect Exception (期待される 例外)となる
以下3つの例はこのコードのみで完結する自己循環型のテスト例で、CalculatePointService
をテストしている訳では無い
try/catchの利用
通常のPHPコードと同様にテスト対象を tryで囲む奴を使う
<?php /** * @test */ public function exception_try_catch() { try { throw new \InvalidArgumentException('message', 200); $this->fail(); // (1)例外がスローされない時はテストを失敗させる } catch (\Throwable $e) { // 指定した例外クラスがスローされているか $this->assertInstanceOf(\InvalidArgumentException::class, $e); // スローされた例外のコードを検証 $this->assertSame(200, $e->getCode()); // スローされた例外のメッセージを検証 $this->assertSame('message', $e->getMessage()); } }
InvalidArgumentException
引数の型が期待する型と一致しなかった場合にスローされる例外。
expectException メソッドの利用
<?php /** * @test */ public function exception_expectedException_method() { // 指定した例外クラスがスローされているか $this->expectException(\InvalidArgumentException::class); // スローされた例外のコードを検証 $this->expectExceptionCode(200); // スローされた例外のメッセージを検証 $this->expectExceptionMessage('message'); throw new \InvalidArgumentException('message', 200); }
@expectedExcepsion アノテーションの利用
作者押しの例外テスト方法
アノテーション(コメント)部分に必要事項を書いてしまう
<?php /** * @test * @expectedException \InvalidArgumentException * @expectedExceptionCode 200 * @expectedExceptionMessage message */ public function exception_expectedException_annotation() { throw new \InvalidArgumentException('message', 200); }
9.1.5.4 購入金額が負数の場合のテスト
テスト例となる CalculatePointService
classのメソッドに以下の様な throw 処理がある、これをテストする
<?php if ($amount < 0) { throw new PreConditionException('購入金額が負の数'); }
ちなみにPreConditionException
だが、ファイルを見ても特に何も書いていないが、ちゃんとしたアプリケーションならここにちゃんと例外を起こした際に書くべき処理を書けって事かな?ここでのテストはちゃんとそこに正しい値が投げられるようになっているかを検証するまでを行う。
app\Exceptions\PreConditionException.php
<?php declare(strict_types=1); namespace App\Exceptions; final class PreConditionException extends \Exception { }
例外テストの具体例
<?php /** * @test * @expectedException \App\Exceptions\PreConditionException * @expectedExceptionMessage 購入金額が負の数 */ public function calcPoint_購入金額が負の数なら例外をスロー() { CalculatePointService::calcPoint(-1); }
マイナスの値がcalcPoint
に投げられた際を検証するために
アノテーションで ’@expectedException'で使用される例外処理先\App\Exceptions\PreConditionException
を指定する
つまり『この異常な値を放り込んだら、この Exceptionにこのメッセージが飛んでる?』って事をテストしてる。当然だが、メッセージをちょっとでも変えるとテストは通らずエラーになる。
9-1-6 テストの前処理・後処理
テスト前にDBに必要なの値の仕込みや、テスト後に値を削除変更する必要がある場合、テストメソッドに書くと煩雑になるので、別途専用にメソッドが用意されている。PHPUnit\Framework\TestClass
にあるテンプレートメソッド。
- 前処理 setUp メソッド
- テスト中 testメソッド
- 後処理 tearDown メソッド
の順で呼ばれて処理される
また、テストクラス毎に呼ばれる以下もある
- setUpBeforeClass メソッド
- tearDownAfterClass メソッド
これらはテストメソッドが属するテストクラス毎に1回だけ呼ばれる
9.1.6.1 テンプレートメソッドの動きを見るテスト
どんな順番で処理が実行されるかを確かめるメソッド群
<?php declare(strict_types=1); namespace Tests\Unit; use App\Services\CalculatePointService; use Tests\TestCase; class TemplateMethodTest extends TestCase { public static function setUpBeforeClass() { parent::setUpBeforeClass(); echo __METHOD__, PHP_EOL; } protected function setUp() { parent::setUp(); echo __METHOD__, PHP_EOL; } /** * @test */ public function テストメソッド1() { echo __METHOD__, PHP_EOL; $this->assertTrue(true); } /** * @test */ public function テストメソッド2() { echo __METHOD__, PHP_EOL; $this->assertTrue(true); } protected function tearDown() { parent::tearDown(); echo __METHOD__, PHP_EOL; } public static function tearDownAfterClass() { parent::tearDownAfterClass(); echo __METHOD__, PHP_EOL; } }
上記のテストを実行すると以下の様な順序でテストが行われる事が確認できる
vagrant@homestead:~/larabook/chapter09$ ./vendor/bin/phpunit tests/Unit/TemplateMethodTest.php PHPUnit 6.5.9 by Sebastian Bergmann and contributors. Tests\Unit\TemplateMethodTest::setUpBeforeClass .Tests\Unit\TemplateMethodTest::setUp Tests\Unit\TemplateMethodTest::テストメソッド1 Tests\Unit\TemplateMethodTest::tearDown . 2 / 2 (100%)Tests\Unit\TemplateMethodTest::setUp Tests\Unit\TemplateMethodTest::テストメソッド2 Tests\Unit\TemplateMethodTest::tearDown Tests\Unit\TemplateMethodTest::tearDownAfterClass Time: 1.1 seconds, Memory: 10.00MB OK (2 tests, 2 assertions)
これらのテンプレートメソッドを使う際はparent
で継承元メソッドを呼ぶことを忘れないようにすること
parent::setUp()
9-1-7 テストの設定
Laravelのルートディレクトリにあうphpunit.xml
ファイルで以下の設定項目の編集が可能
詳細な設定に関しては PHPUnit の公式マニュアルを参照のこと