はじめに
PHPUnitで例外処理のテストコードを書く方法を紹介します。
というのも、色々調べるた結果、自分がやりたい書き方が中々見つからなかったんですよね。
なので、今回は自分が採用した例外テストの書き方を記事にすることにしました。
本記事の対象
・PHPUnitで例外テストをしたい方
・PHPを学習中の方
動作環境
Computer: Mac Mini 2018
OS: macOS Big Sur version 11.1
PHP: version 7.4.11
PHPUnit: version 9.5.0
テストケース
IDの値が1より小さい時は例外エラーとする
「データベースから取得したidの値は1以上である」という制限を補償することを想定しています。
値をintで制限しても、1より小さい値が間違って代入できてしまうのは困りますよね。
なので、IDの値が1より小さい時は例外エラーとして処理できているかをテストします。
テスト環境構築
PHPUnitの準備
※composerがインストール済であることを前提にしています。
作業中のディレクトリで以下を実行してください。
$ composer require "PHPUnit/PHPUnit"
これで作業中のディレクトリに以下3つが作成されます。
・vendorディレクトリ
・composer.jsonファイル
・composer.lock
namespace(名前空間)の指定
作成されたcomposer.jsonファイルの内容を以下のようにしてください。
{
"require": {
"phpunit/phpunit": "^9.5"
},
"autoload": {
"psr-4":
"ValueObjects\\": "ValueObjects/",
"Tests\\": "tests/"
}
}
}
これで、作業ディレクトリのValueObjectsディレクトリとtestsディレクトリを名前空間で識別できるようにしました。
composerの設定リロード
composer.jsonを編集した後は以下のコマンドを実行してください。
以下のコマンドを実行しないと、composer.jsonの変更内容が反映されません。
$ composer dump-autoload
テストコードの実装
テスト対象のコードを実装する前にテストコードを実装します。
テストコードファイルを作成
作業ディレクトリにtestsディレクトリを作成し、その下にIdObjectTest.phpファイルを作成します。
以下のコマンドを作業ディレクトリで実行してください
# ディレクトリの作成
$ mkdir tests
# テストコードファイルの作成
$ touch tests/IdObjectTest.php
テストコードファイルにベースコードを記述
作成したIdObjectTest.phpに以下を追記してください。
<?php
use PHPUnit\Framework\TestCase;
class IdObjectTest extends TestCase
{
}
テストコードの実装
では、__「IdObjectクラスの引数に0を渡すと、例外処理されると例外エラーとなることをテストする」__テストを実装します。
<?php
use PHPUnit\Framework\TestCase;
// IdObjectクラスがValueObjectsディレクトリ配下のあることを想定しています。
// このuse文を記述しないとIdObjectが何か分からないのでテストができません。
use ValueObjects\IdObject;
// このuse文ではIdObjectの例外処理を行うクラスを読み込んでいます。
use ValueObjects\IdException;
class IdObjectTest extends TestCase
{
/*
* IdObjectに0を渡すと、例外処理が発生することをテストする
*/
public function testExceptionWhenIdIsZero(){
$this->expectException(IdException::class);
$id = new IdObject(0);
}
}
まだIdObjectクラスもIdExceptionクラスも生成していないので、テストはできません。
例外クラスの作成
IdObject専用の例外クラスを作成します。
IdException.phpファイルの作成
作業中のディレクトリで以下のコマンドを実行してください。
ValueObjectsディレクトリの下にIdException.phpを作成します。
# ここでValueObjectsディレクトリを作成しています。
$ mkdir ValueObjects
# ここでValueObjectsディレクトリの下にIdException.phpを作成しています。
$ touch ValueObjects/IdException.php
IdException.phpにベースコードを記載
作成したIdException.phpの内容を以下のようにしてください。
<?php
// ここで名前空間を指定してください
namespace ValueObjects;
// Exceptionクラスを継承してください。
// IdObject専用の例外処理を行うクラスであるとわかるクラス名にしています。
class IdException extends \Exception
{
}
これで、例外クラスが作成できました。
テスト対象の実装
IdObject.phpファイルの作成
作業中のディレクトリで以下のコマンドを実行してください。
ValueObjectsディレクトリの下にIdObject.phpが作成されます。
$ touch ValueObjects/IdObject.php
IdObject.phpにコードを実装
<?php
namespace ValueObjects;
class IdObject
{
private $value;
// この下の@throws IdExceptionを忘れたらテストでエラーになるので注意してください。
/**
* IdObject constructor.
* @param $value
* @throws IdException
*/
public function __construct($value)
{
// ここで、valueが1より小さい時にIdExceptionクラスを利用して例外エラーを発生させています。
if($value < 1){
throw new IdException();
}
// 例外が発生しなければ、クラス内の変数に受け取った値を格納します。
$this->value = $value;
}
// クラス内の変数の値を取得できるようにしています。
public function value(){
return $this->value;
}
}
テスト実施
例外テストの実施
では、以下のコマンドでテストを実施してください。
tests/ディレクトリ配下のテストコードを実行して、テストを行います。
--colorは結果に背景色をつけてくれるので見やすくなります。
$ php vendor/bin/phpunit --color tests/
テスト結果
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 00:00.021, Memory: 4.00 MB
OK (1 test, 1 assertion)
OKと表示されているので大丈夫ですね。
IdObjectクラスのオブジェクトが生成されるテストコード実装
では、IdObjectTest.phpに以下のメソッドを追加して、値が1以上のIdObjectクラスを生成できるかテストしましょう
.
.
.
public function testPassWhenIdIsUpperOne(){
$id = new IdObject(1);
$this->assertEquals(1, $id->value());
}
テスト実施
$ php vendor/bin/phpunit --color tests
テスト結果
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.
.. 2 / 2 (100%)
Time: 00:00.018, Memory: 4.00 MB
OK (2 tests, 2 assertions)
2項目のテストが成功したことを確認できました。
例外時のメッセージを自作
ここまでで例外処理のテストを行うことはできたと思います。
しかし、複数の条件で異なるメッセージを表示したい場合などもありますよね。
今回はIDが0の時と、マイナスの時で別のメッセージが表示されるうにしてテストしたいと思います。
IdException.phpに追記
ベースだけ作成していたIdException.phpの内容を以下のようにしてください。
<?php
// ここで名前空間を指定してください
namespace ValueObjects;
// Exceptionクラスを継承してください。
// IdObject専用の例外処理を行うクラスであるとわかるクラス名にしています。
class IdException extends \Exception
{
// エラーが発生した時のエラーメッセージを予め用意しています。
public const MSG_ZERO = 'you can not set zero to id value';
public const MSG_NEGATIVE = 'you can not set negative to id value';
//このクラスが呼ばれた時に親であるExceptionクラスのコンストラクタを呼び出すようにしています。
public function __construct($message = "", $code = 0, $previous = null)
{
parent::__construct($message, $code, $previous);
}
/**
* @param $message
* @throws IdException
* このメソッドが実際に例外エラーを発生させています。
*/
public static function raiseExceptionIdValueIsInvalid($message){
throw new IdException($message);
}
}
IdObject.phpを変更
IdObject.phpの内容を以下のようにしてください。
値が0の時、マイナスの時で条件を分割しています。
<?php
namespace ValueObjects;
class IdObject
{
private $value;
// このドキュメント文を忘れるとテストでエラーになるので注意してください。
// この次の行の*(アスタリスク)が2つなのも注意してください。
/**
* IdObject constructor.
* @param $value
* @throws IdException
*/
public function __construct($value)
{
// エラーが発生する条件を分岐します。
// 0を受け取った時はMSG_ZEROの内容がエラーとして出力されるようにしています。
if($value === 0){ IdException::raiseExceptionIdValueIsInvalid(IdException::MSG_ZERO); }
// マイナスの値を受け取った時はMSG_NEGATIVEの内容がエラーとして出力されるようにしています。
if($value < 0){ IdException::raiseExceptionIdValueIsInvalid(IdException::MSG_NEGATIVE); }
// 例外が発生しなければ、クラス内の変数に受け取った値を格納します。
$this->value = $value;
}
// クラス内の変数の値を取得できるようにしています。
public function value(){
return $this->value;
}
}
NGとなるテストコードの実装
では、わざとテストが失敗するようにして、表示されたエラーの内容を確認しましょう。
IdObjectTest.phpに以下のメソッドを追加してください。
.
.
.
// $this->expectException(IdException::class);がないのでテストはNGになります。
public function testDummyExceptionIdIsZero(){
$id = new IdObject(0);
}
NGとなるテストの実施
お馴染みとなったコマンドでテストを実行してください。
$ php vendor/bin/phpunit --color tests/
テスト結果
次のメッセージが表示されていたら、問題なくメッセージ表示もできています。
ValueObjects\IdException: you can not set zero to id value
もう1つのテスト条件をIdObjectTest.phpに追加
IdObjectTest.phpの内容を以下に変更してください。
.
.
.
# 変更前
public function testDummyExceptionIdIsZero(){
$id = new IdObject(0);
}
↓↓↓↓↓↓↓↓↓↓↓↓↓↓このように変更してください↓↓↓↓↓↓↓↓↓↓↓↓↓↓
# 変更後
public function testDummyExceptionIdIsNegative(){
$id = new IdObject(-1);
}
テスト結果
次のメッセージが表示されていたらOKです。
ValueObjects\IdException: you can not set negative to id value
出力されているエラーからテスト結果を判定する
ここまでは、例外エラーが発生していることしかテストできていません。
また、発生しているエラーメッセージを確認するために、わざわざNGとなるテストコードを1つずつ記載していました。
しかし、実際にはもっとたくさんの例外テストやメッセージを判定するケースがあります。
なので、そのケースに沿ってテストコードを実装してみましょう。
###IdObjectTest.phpの変更
IdObjectTest.phpの内容を次のように変更してください。
<?php
use PHPUnit\Framework\TestCase;
// IdObjectクラスがValueObjectsディレクトリ配下のあることを想定しています。
// このuse文を記述しないとIdObjectが何か分からないのでテストができません。
use ValueObjects\IdObject;
// このuse文ではIdObjectの例外処理を行うクラスを読み込んでいます。
use ValueObjects\IdException;
class IdObjectTest extends TestCase
{
/*
* IdObjectに0を渡すと、例外処理が発生することをテストする
*/
public function testExceptionWhenIdIsZero(){
try{
$id = new IdObject(0);
}catch( Exception $ex ){
// ここで例外が発生した時のエラーメッセージを取得しています。
$msg = $ex->getMessage();
// ここでエラーメッセージが想定通りのものかをチェックしています。
$this->assertEquals(IdException::MSG_ZERO, $msg);
}
}
/*
* IdObjectにマイナスの値を渡すと、例外処理が発生することをテスト
*/
public function testExceptionWhenIdIsNegative(){
try{
$id = new IdObject(-1);
}catch (Exception $ex){
$msg = $ex->getMessage();
$this->assertEquals(IdException::MSG_NEGATIVE, $msg);
}
}
/*
* IdObjectに1を渡すことができるのをテストする
*/
public function testPassWhenIdIsUpperOne(){
$id = new IdObject(1);
$this->assertEquals(1, $id->value());
}
}
テスト実施
$ php vendor/bin/phpunit
テスト結果
OK (3 tests, 3 assertions)
これでテスト結果がOKにもなり、例外テストを実施することもできます。
さらに、例外が発生した時のメッセージでテスト結果を判定すれば確実ですよね。
追加情報
私はtry catch文で実装しましたが、よりシンプルな実装方法を@juve_534 さんに教えていただきました。
※ありがたい
参考記事:
PHPUnitで例外をテスト
最後に
テストコードを書くメリットは、__メソッド毎の役割を明確にするクセがつくこと__だと思います。
というのも、役割を考えずにメソッドを作った結果、1つのメソッドに膨大な量のコードが集まってしまったんですね。
その時は過去の自分を恨みましたよ。
場合によって、メソッド内のコード量が多くなってしまうのは仕方ありません。
しかし、下位のメソッドが大きくなってしまうのは、設計や役割を理解していないケースが多いです。
そうなると、自分以外の人がみた時、3ヶ月後の自分が見た時も全く理解できません。
テストコードを書くクセがついていれば、1つのメソッドの役割やテストのしやすさを考えるようになります。
結果的に完成度の高い下位モジュールがプロダクトを強固なものにすると自分は信じています。
最後まで読んでいただきありがとうございます。
この記事があなたのお役に立てると嬉しいです