CodeIgniter3でci-phpunit-test 2.xを利用したUnitTestいろいろ②（Migrationの利用）

# 概要

CodeIgniter3の機能にあるMigrationを利用します。
ここでは`ci-phpunit-test`で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)に載っています。

# ci-phpunit-test用の仕掛けとは

`ci-phpunit-test`を利用したModelクラスのUnit Testで実現したいなと考えていること。

- phpunit実行時に毎回DBが初期化されること
- 予期せぬデータが投入された状態ではなく、予期した状態で再現性のあるDBアクセスを含んだUnit Testを実行できること
- 開発環境(ENVIRONMENT = development)を侵害せずにUnit Testを行えること

こういったことを実現したくて、何かいい方法がないか模索し、とりあえず形になったものをこのQiita記事にまとめました。
※ここでは、上記のようなphpunitを実現するための、Migrationの構築がメインになります。

# ソースの構成

```shell-session
.application
├── config
│   ├── development         # ← 開発環境用
│   │   └── database.php
│   ├── testing             # ← phpunit（テスト）用
│   │   └── database.php
├── controllers
│   └── Migrate.php
├── migrations
│   └── 001_add_gmo_result_notification_logs.php
```

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

## config

CodeIgniterのデフォルトとして、ENVIRONMENT=developmentで動作し、ci-phpunit-testの動作時はENVIRONMENT=testingで動作する。

**参考**
https://github.com/kenjis/ci-phpunit-test/blob/master/docs/HowToWriteTests.md#testing-environment

そのため、`development`と`testing`専用のデータベースアクセス用の設定ファイル`database.php`を用意しておけば、開発環境への影響無しに、phpunitの実行によってデータベースを初期化し、CRUDを実行して再現性のあるUnit Testが実行可能となります。

config配下にdevelopmentとtestingの2つのディレクトリを作成し、database.phpファイルをコピーして、application/config/database.phpを削除しています。
config配下にある各設定ファイルの挙動として、以下優先順位でcodeigniterが読み込むようです。

1. application/config/{設定}.php
2. application/config/{ENVIRONMENT}/{設定}.php

そのため、application/config/database.phpが残ったままだと、developmentやtesting配下に作ったdatabase.phpを読み込んでくれないため、直下のdatabase.phpファイルは削除しています。

**参考**
https://codeigniter.jp/user_guide/3/general/environments.html
https://codeigniter.jp/user_guide/3/libraries/config.html

```php:application/config/development/database.php
<?php
defined('BASEPATH') OR exit('No direct script access allowed');

$active_group = 'default';
$query_builder = TRUE;

$db['default'] = array(
	'dsn'	=> '',
	'hostname' => 'localhost',
	'username' => 'yu',
	'password' => 'yupass',
	'database' => 'yudb',
	'dbdriver' => 'mysqli',
	'dbprefix' => '',
	'pconnect' => FALSE,
	'db_debug' => (ENVIRONMENT !== 'production'),
	'cache_on' => FALSE,
	'cachedir' => '',
	'char_set' => 'utf8mb4',
	'dbcollat' => 'utf8mb4_general_ci',
	'swap_pre' => '',
	'encrypt' => FALSE,
	'compress' => FALSE,
	'stricton' => FALSE,
	'failover' => array(),
	'save_queries' => TRUE
);
```

```php:application/config/testing/database.php
<?php
defined('BASEPATH') OR exit('No direct script access allowed');

$active_group = 'phpunit';
$query_builder = TRUE;

// PHPUnitから利用するデータベース定義
$db['phpunit'] = array(
	'dsn'	=> '',
	'hostname' => 'localhost',
	'username' => 'yu',
	'password' => 'yupass',
	'database' => 'phpunitdb',
	'dbdriver' => 'mysqli',
	'dbprefix' => '',
	'pconnect' => FALSE,
	'db_debug' => (ENVIRONMENT !== 'production'),
	'cache_on' => FALSE,
	'cachedir' => '',
	'char_set' => 'utf8mb4',
	'dbcollat' => 'utf8mb4_general_ci',
	'swap_pre' => '',
	'encrypt' => FALSE,
	'compress' => FALSE,
	'stricton' => FALSE,
	'failover' => array(),
	'save_queries' => TRUE
);
```

## Migrationを管理するMigrateコントローラクラス

基本的には`migration`ライブラリのマイグレーション用メソッドを実行するだけのコントローラクラスです。
一応Webアプリケーションなので、ブラウザからマイグレーションが実行されないように、コマンドラインからのみのリクエストを受け付けるようにしています。

```php:application/controllers/Migrate.php
<?php
defined('BASEPATH') OR exit('No direct script access allowed');

/**
 * Migration用のControllerクラス
 */
class Migrate extends MY_Controller {

    /**
     * 全ての利用者がアクセス可能なControllerとして定義
     * 一応コンストラクタ内でコマンド実行じゃなければエラーにするようにしている。
     */
    protected $access = "*";

    function __construct()
    {   
        parent::__construct();
        if(!$this->input->is_cli_request()) {
            show_404();
            exit;
        }   
        $this->load->library('migration');
    }   

    /**
     * currentバージョンにマイグレーションする
     */
    function current()
    {   
        if ($this->migration->current()) {
            log_message('error', 'Migration Success.');
        } else {
            log_message('error', $this->migration->error_string());
        }   
    }   

    /**
     * 指定されたバージョンにロールバックする
     */
    function rollback($version)
    {   
        if ($this->migration->version($version)) {
            log_message('error', 'Migration Success.');
        } else {
            log_message('error', $this->migration->error_string());
        }   
    }   

    /**
     * 最新バージョンにマイグレーションする
     */
    function latest()
    {   
        if ($this->migration->latest()) {
            log_message('error', 'Migration Success.');
        } else {
            log_message('error', $this->migration->error_string());
        }   
    }

}
```

## migrationsのファイル

application/migrations配下に定義するマイグレーション用のファイルについては、次のどちらかのルールに基づいたファイル名をつけてバージョンが分かるようにする必要があります。

1. シーケンス番号をファイル名の先頭に付与（シーケンス番号は001から始まる連番する）
    1. 例： 001_add_gmo_result_notification_logs.php
    2. application/config/migration.phpの定義
        3. `$config['migration_type'] = 'sequential';`
1. Timestampをファイル名の先頭に付与（YYYYMMDDHHIISS）
    1. 例：20201011100000_add_gmo_result_notification_logs.php
    2. application/config/migration.phpの定義
        3. `$config['migration_type'] = 'timestamp';`

**参考**
https://codeigniter.jp/user_guide/3/libraries/migration.html#id2

```php:application/migrations/001_add_gmo_result_notification_logs.php
<?php

defined('BASEPATH') OR exit('No direct script access allowed');

/**
 * GMO-PGの結果通知プログラムによって渡されるパラメータ情報などを保持するログテーブル 
 */
class Migration_Add_Gmo_result_notification_logs extends CI_Migration
{
    public function up()
    {
        $this->dbforge->add_field([
            'id'       => [
                'type'           => 'int',
                'unsigned'       => TRUE,
                'auto_increment' => TRUE
            ],
            'pay_method'    => [
                'type'       => 'varchar',
                'constraint' => '10',
            ],
            'parameter' => [
                'type'       => 'json'
            ],
            'create_date' => [
                'type'       => 'datetime'
            ]
        ]);
        $this->dbforge->add_key('id', TRUE);
        $this->dbforge->create_table('gmo_result_notification_logs');
    }

    public function down()
    {
        $this->dbforge->drop_table('gmo_result_notification_logs');
    }
}
```

# 実際にMigrationファイルを利用してテーブルを作成

まずはDBの状態を確認。
developmentとtestingの2つのデータベース接続先はすでに存在している前提です。

```shell-session
# mysql -u root -p
Enter password: 

MariaDB [(none)]> connect yudb;
MariaDB [yudb]> show tables;
Empty set (0.001 sec)
```

今のところdevelopment用のデータベースには何もテーブルが存在しない状態です。
※application/controllers/Migrate.phpのコントローラを利用してコマンドからマイグレーションを実行します。
　この場合、CodeIgniterのデフォルトのENVIRONMENTであるdevelopmentの方の接続情報が利用されます。

```shell-session
# cd /var/www/html/codeigniter3/public/
# php index.php migrate/current
# mysql -u root -p
Enter password: 

MariaDB [(none)]> connect yudb;
MariaDB [yudb]> show tables;

+------------------------------+
| Tables_in_yudb               |
+------------------------------+
| gmo_result_notification_logs |
| migrations                   |
+------------------------------+
2 rows in set (0.000 sec)

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

上記 migrationsテーブルの取得結果 version=1と取得できているのは、application/config/migration.phpの`$config['migration_version'] = 1;`の番号です。
システム改修を行って、バージョンを変更する場合は、この設定を変更してマイグレーションを実行します。