PHPUnitで例外テスト

## はじめに
PHPUnitで例外処理のテストコードを書く方法を紹介します。
というのも、色々調べるた結果、自分がやりたい書き方が中々見つからなかったんですよね。
なので、今回は自分が採用した例外テストの書き方を記事にすることにしました。

## 本記事の対象
・PHPUnitで例外テストをしたい方
・PHPを学習中の方

## 動作環境
```bash:動作環境
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がインストール済であることを前提にしています。*__

作業中のディレクトリで以下を実行してください。

```bash:terminal
$ composer require "PHPUnit/PHPUnit"
```
これで作業中のディレクトリに以下３つが作成されます。
・vendorディレクトリ
・composer.jsonファイル
・composer.lock

### namespace(名前空間)の指定
作成されたcomposer.jsonファイルの内容を以下のようにしてください。

```json:compose.json
{
  "require": {
    "phpunit/phpunit": "^9.5"
  },
  "autoload": {
    "psr-4":
      "ValueObjects\\": "ValueObjects/",
      "Tests\\": "tests/"
    }
  }
}
```
これで、作業ディレクトリのValueObjectsディレクトリとtestsディレクトリを名前空間で識別できるようにしました。

### composerの設定リロード
composer.jsonを編集した後は以下のコマンドを実行してください。
__*以下のコマンドを実行しないと、composer.jsonの変更内容が反映されません。*__

```bash:terminal
$ composer dump-autoload
```

## テストコードの実装
テスト対象のコードを実装する前にテストコードを実装します。

### テストコードファイルを作成
作業ディレクトリにtestsディレクトリを作成し、その下にIdObjectTest.phpファイルを作成します。
以下のコマンドを作業ディレクトリで実行してください

```bash:terminal
# ディレクトリの作成
$ mkdir tests

# テストコードファイルの作成
$ touch tests/IdObjectTest.php
```

### テストコードファイルにベースコードを記述
作成したIdObjectTest.phpに以下を追記してください。

```php:IdObjectTest.php
<?php

use PHPUnit\Framework\TestCase;

class IdObjectTest extends TestCase
{

}
```

### テストコードの実装
では、__*「IdObjectクラスの引数に0を渡すと、例外処理されると例外エラーとなることをテストする」*__テストを実装します。

```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(){
        $this->expectException(IdException::class);
        $id = new IdObject(0);
    }
}
```
__*まだIdObjectクラスもIdExceptionクラスも生成していないので、テストはできません。*__

## 例外クラスの作成
IdObject専用の例外クラスを作成します。
### IdException.phpファイルの作成
作業中のディレクトリで以下のコマンドを実行してください。
ValueObjectsディレクトリの下にIdException.phpを作成します。

```bash:terminal
# ここでValueObjectsディレクトリを作成しています。
$ mkdir ValueObjects

# ここでValueObjectsディレクトリの下にIdException.phpを作成しています。
$ touch ValueObjects/IdException.php
```

### IdException.phpにベースコードを記載
作成したIdException.phpの内容を以下のようにしてください。

```php:IdException.php
<?php

// ここで名前空間を指定してください
namespace ValueObjects;

// Exceptionクラスを継承してください。
// IdObject専用の例外処理を行うクラスであるとわかるクラス名にしています。
class IdException extends \Exception
{
    
}
```
これで、例外クラスが作成できました。

## テスト対象の実装
### IdObject.phpファイルの作成
作業中のディレクトリで以下のコマンドを実行してください。
ValueObjectsディレクトリの下にIdObject.phpが作成されます。

```bash:terminal
$ touch ValueObjects/IdObject.php
```

#### IdObject.phpにコードを実装
```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は結果に背景色をつけてくれるので見やすくなります。

```bash:terminal
$ php vendor/bin/phpunit --color tests/
```

### テスト結果
```bash:terminal
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クラスを生成できるかテストしましょう

```php:IdObjectTest.php
.
.
.
public function testPassWhenIdIsUpperOne(){
        $id = new IdObject(1);
        $this->assertEquals(1, $id->value());
    }
```

### テスト実施
```bash:terminal
$ php vendor/bin/phpunit --color tests
```

### テスト結果
```bash:terminal
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)
```
２項目のテストが成功したことを確認できました。


## 例外時のメッセージを自作
ここまでで例外処理のテストを行うことはできたと思います。
しかし、複数の条件で異なるメッセージを表示したい場合などもありますよね。
今回はIDが０の時と、マイナスの時で別のメッセージが表示されるうにしてテストしたいと思います。

### IdException.phpに追記
ベースだけ作成していたIdException.phpの内容を以下のようにしてください。

```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:IdObject.php
<?php

namespace ValueObjects;

class IdObject
{
    private $value;

    // このドキュメント文を忘れるとテストでエラーになるので注意してください。
    // この次の行の*(アスタリスク)が２つなのも注意してください。
    /**
     * 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に以下のメソッドを追加してください。

```php:IdObjectTest.php
.
.
.
// $this->expectException(IdException::class);がないのでテストはNGになります。
public function testDummyExceptionIdIsZero(){
        $id = new IdObject(0);
    }
```

### NGとなるテストの実施
お馴染みとなったコマンドでテストを実行してください。

```bash:terminal
$ php vendor/bin/phpunit --color tests/
```

### テスト結果
次のメッセージが表示されていたら、問題なくメッセージ表示もできています。

```text:terminal
ValueObjects\IdException: you can not set zero to id value
```

### もう１つのテスト条件をIdObjectTest.phpに追加
IdObjectTest.phpの内容を以下に変更してください。

```php:IdObjectTest.php
.
.
.
# 変更前
public function testDummyExceptionIdIsZero(){
        $id = new IdObject(0);
    }

↓↓↓↓↓↓↓↓↓↓↓↓↓↓このように変更してください↓↓↓↓↓↓↓↓↓↓↓↓↓↓

# 変更後
public function testDummyExceptionIdIsNegative(){
        $id = new IdObject(-1);
    }
```

### テスト結果
次のメッセージが表示されていたらOKです。

```text:terminal
ValueObjects\IdException: you can not set negative to id value
```

## 出力されているエラーからテスト結果を判定する
ここまでは、例外エラーが発生していることしかテストできていません。
また、発生しているエラーメッセージを確認するために、わざわざNGとなるテストコードを１つずつ記載していました。
しかし、実際にはもっとたくさんの例外テストやメッセージを判定するケースがあります。
なので、そのケースに沿ってテストコードを実装してみましょう。

###IdObjectTest.phpの変更
IdObjectTest.phpの内容を次のように変更してください。

```php:IdObjectText.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());
    }

}

```

### テスト実施
```bash:terminal
$ php vendor/bin/phpunit
```

### テスト結果
```text:terminal
OK (3 tests, 3 assertions)
```
これでテスト結果がOKにもなり、例外テストを実施することもできます。
さらに、例外が発生した時のメッセージでテスト結果を判定すれば確実ですよね。

## 追加情報
私はtry catch文で実装しましたが、よりシンプルな実装方法を[@juve_534 さん](https://qiita.com/juve_534)に教えていただきました。
※ありがたい

参考記事：
[PHPUnitで例外をテスト](https://qiita.com/juve_534/items/b76fd7ec084d28db885c#%E6%B0%97%E3%81%AB%E3%81%AA%E3%82%8B%E3%81%93%E3%81%A8)


## 最後に
テストコードを書くメリットは、__*メソッド毎の役割を明確にするクセがつくこと*__だと思います。
というのも、役割を考えずにメソッドを作った結果、１つのメソッドに膨大な量のコードが集まってしまったんですね。
その時は過去の自分を恨みましたよ。

場合によって、メソッド内のコード量が多くなってしまうのは仕方ありません。
しかし、下位のメソッドが大きくなってしまうのは、設計や役割を理解していないケースが多いです。
そうなると、自分以外の人がみた時、3ヶ月後の自分が見た時も全く理解できません。
テストコードを書くクセがついていれば、１つのメソッドの役割やテストのしやすさを考えるようになります。
結果的に完成度の高い下位モジュールがプロダクトを強固なものにすると自分は信じています。

最後まで読んでいただきありがとうございます。
この記事があなたのお役に立てると嬉しいです:relaxed: