CodeIgniter3でci-phpunit-test 2.xを利用したUnitTestいろいろ③（DBアクセスを伴うModelクラスのUnit Test）

# 概要

ci-phpunit-testで、DBアクセスを伴うModelクラスのUnit Testを実装します。

CodeIgniter3でci-phpunit-test 2.xの環境を用意する方法については、[CodeIgniter3 + PHPUnit 9.4.0 + ci-phpunit-test dev-2.x を利用した Unit Test環境の整備](https://qiita.com/You_name_is_YU/items/df51b52820af9e41f2ec)に載っています。

このModelクラスのUnit Testの実装では、事前に[CodeIgniter3でci-phpunit-test 2.xを利用したUnitTestいろいろ②（Migrationの利用）](https://qiita.com/You_name_is_YU/items/21cb3f1c0f304fbb3885)の実装をしておく必要があります。

# ソースの構成

```shell-session
.application
├── models
│   └── Gmo_result_notification_logs_model.php
├── tests
│   ├── models
│   │   └── Gmo_result_notification_logs_model_test.php
```

# 各ソースの内容とポイント

## テスト対象となるDBアクセスを含むモデルクラス

特に変わったことはしていない、通常のモデルクラスです。
ログを削除する処理は基本的に必要ないですが、UnitTestがしやすいように削除処理も入れています。

```php:application/models/Gmo_result_notification_logs_model.php
<?php

/**
 * GMOから実行される結果通知プログラムのログ情報モデルクラス
 */
class Gmo_result_notification_logs_model extends CI_Model {

    /** ID（自動採番） */
    public $id;
    /** 決済方法（credit:クレジット，cvs:コンビニ ...etc） */
    public $pay_method;
    /** 結果通知プログラムに送られてきたパラメータをJSON変換したもの */
    public $parameter;
    /** 登録日時 */
    public $create_date;

    /** モデルで操作するテーブル名 */
    private $_table = 'gmo_result_notification_logs';

    public function __construct() {
        parent::__construct();
    }

    /**
     * ログを登録する
     */
    public function insert_log() {
        $this->db->insert($this->_table, $this);
    }

    /**
     * ログを削除する
     */
    public function delete_log() {
        $this->db->delete($this->_table, array('id' => $this->id));
    }

    /**
     * 最新１０件のログを取得する
     */
    public function get_last_ten_logs() {
        $query = $this->db->limit(10)
            ->order_by('id', 'desc')
            ->get($this->_table, 10);
        return $query->result();
    }
}
```

## DBアクセスを含むModelクラスのテストクラス

**ポイント**

- setUpBeforeClass() でMigrationを実行し、テスト用のDBの初期構築を行っている。
    - ci-phpunit-testで実行されるとき、ENVIRONMENT=testingで実行されるため、ここでloadされるデータベースはapplication/config/testing/database.phpの定義を参照している。
- tearDownAfterClass() でMigrationのrollbackを実行し、version=0の状態に戻している。
    - 各CI_Migrationを継承するクラスの`down()`が実行されて最初の状態に戻るイメージ 

```php:application/tests/models/Gmo_result_notification_logs_model_test.php
<?php

/**
 * Gmo_result_notification_logs_modelクラスのテスト
 */
class Gmo_result_notification_logs_model_test extends TestCase
{
    /**
     * テストクラス内の初回（１回）のみ実行する初期化処理
     */
    public static function setUpBeforeClass():void
    {
        // テスト用DBを初期化（migration）して利用
        $CI =& get_instance(); 
        $CI->load->database();
        $CI->load->library('migration');
        $CI->migration->current();
    }

    /**
     * テスト初期処理（各テストメソッドの実行前処理）
     */
    public function setUp():void
    {
        $this->resetInstance();

        $this->CI->load->model('Gmo_result_notification_logs_model');
        $this->obj = $this->CI->Gmo_result_notification_logs_model;
    }

    /**
     * テストクラス内の最後（１回）のみ実行する終了処理
     */
    public static function tearDownAfterClass():void
    {
        $CI =& get_instance(); 
        $CI->load->library('migration');
        $CI->migration->version(0);
    }

    /**
     * @test
     */
    public function ログの登録が正常に完了すること():void
    {
        $this->obj->id = 999;
        $this->obj->pay_method = 'credit';
        $this->obj->parameter = '{"aaa": "bbb", "ccc": "ddd"}';
        $this->obj->create_date = date('Y-m-d h:m:s');

        $this->obj->insert_log();
        $this->assertTrue(true); // ここまできたらOK
    }

    /**
     * @test
     * @depends ログの登録が正常に完了すること
     */
    public function 登録済みのログが削除できること():void
    {
        $this->obj->id = 999;
        $this->obj->delete_log();
        $this->assertTrue(true); // ここまできたらOK
    }

    /**
     * @test
     * @depends 登録済みのログが削除できること
     */
    public function 登録した順序の降順で取得できること():void
    {
        $insert_values = array(
            array(
                'id'=> 1,
                'pay_method'=> 'credit',
                'parameter'=> '{"aaa": "bbb", "ccc": "ddd"}',
                'create_date'=> date('Y-m-d h:m:s')
            ),
            array(
                'id'=> 2,
                'pay_method'=> 'cvs',
                'parameter'=> '{"eee": "ffff", "gggg": "hhh"}',
                'create_date'=> date('Y-m-d h:m:s')
            ),
            array(
                'id'=> 3,
                'pay_method'=> 'paypay',
                'parameter'=> '{"iii": "jjj", "kkk": "lll"}',
                'create_date'=> date('Y-m-d h:m:s')
            )
        );

        foreach ($insert_values as $value) {
            $this->obj->id = $value['id'];
            $this->obj->pay_method = $value['pay_method'];
            $this->obj->parameter = $value['parameter'];
            $this->obj->create_date = $value['create_date'];

            $this->obj->insert_log();
        }

        // ログを取得
        $logs = $this->obj->get_last_ten_logs();

        // 降順に並び替え
        $reverse_values = array_reverse($insert_values);

        for ($i = 0; $i < count($reverse_values); $i++) { 
            $this->assertEquals($reverse_values[$i]['id'], $logs[$i]->id);
            $this->assertEquals($reverse_values[$i]['pay_method'], $logs[$i]->pay_method);
            $this->assertEquals($reverse_values[$i]['parameter'], $logs[$i]->parameter);
            $this->assertEquals($reverse_values[$i]['create_date'], $logs[$i]->create_date);
        }
    }

}
```

# 実際のテスト実行

まだ何もしていない状態では、ENVIRONMENT=testingのDB接続先となっている`phpunitdb`のデータベース内には何もテーブルがない状態。

```shell-session
# mysql -u root -p
MariaDB [(none)]> connect phpunitdb;
MariaDB [phpunitdb]> show tables;
Empty set (0.001 sec)
```

phpunitを実行してテストを動かす。

```shell-session
# cd /var/www/html/codeigniter3/application/tests
# ../../vendor/bin/phpunit --testdox

PHPUnit 9.4.0 by Sebastian Bergmann and contributors.

Warning:       No code coverage driver available
Warning:       Your XML configuration validates against a deprecated schema.
Suggestion:    Migrate your XML configuration using "--migrate-configuration"!

Gmo_api_model_test
 ✔ G m o決済リンク urlが正常に取得できること
 ✔ G m o決済リンク url取得失敗の詳細が exceptionで伝播されること
 ✔ G m oカード会員編集 urlが正常に取得できること

Gmo_result_notification_logs_model_test
 ✔ ログの登録が正常に完了すること
 ✔ 登録済みのログが削除できること
 ✔ 登録した順序の降順で取得できること

Time: 00:00.047, Memory: 8.00 MB

OK (6 tests, 29 assertions)
```

テストを実行したので、データベースの状態を再確認してみる。

```shell-session
# mysql -u root -p
MariaDB [(none)]> connect phpunitdb;
MariaDB [phpunitdb]> show tables;

+---------------------+
| Tables_in_phpunitdb |
+---------------------+
| migrations          |
+---------------------+
1 row in set (0.000 sec)

MariaDB [phpunitdb]> select * from migrations;
+---------+
| version |
+---------+
|       0 |
+---------+
1 row in set (0.002 sec)
```

このように、UnitTest自体は正常に完了し、phpunitdbにはmigrationsテーブルのみが残った状態となっています。（本当は作られているはずの`gmo_result_notification_logs`テーブルが残っていない。）
version=0となっており、`tearDownAfterClass()`で指定したバージョンまでrollbackされていることが確認できる。

# 最後に

今回の対応では、特にテーブルを作った後、初期データのロードなどは行っていないため、毎回初期化しても耐えられるレベルのものでした。
初期データのロードが大量にあったりする場合は、テストクラス内で依存するデータのみをロードするようにしたり、データに依存しないテスト（Mockを利用したテスト）にすることで、規模が大きくなったときに対応する必要がありそうです。

とはいえ、小さいサービスのうちは、こういったテストを作っておくことで、Migrationの検証にもなるし、いいかなぁとは思っています。