PHPのDBユニットテスト

PhpUnitDBUnit拡張が追加できない特殊な環境向け(MySQL限定)のDBユニットテストもどきを作ってみた。

  • DbTestUtil.php
<?php

/**
 * DBを用いたユニットテストのヘルパークラス
 * 
 * ※PhpUnitのDBUnit拡張が追加できない特殊な環境向け(MySQL限定)
 * ※DBダンプデータは下記コマンドで取得する
 * mysqldump --xml -t -u [ユーザ名] --password=[パスワード] 
 *   --port [ポート番号] [データベース名] [テーブル名] > [インポートするファイル名]
 */
class DbTestUtil
{
    /** 
     * 接続情報
     * 
     * @var string $_dsn
     */
    const DSN = "mysql:host=192.168.XX.XX;port=3306;dbname=XXXX;charset=utf8";
    /** 
     * ユーザ名 
     * 
     * @var string $_username
     */
    const USERNAME = "XXX";
    /** 
     * パスワード 
     * 
     * @var string $_password
     */
    const PASSWORD = "XXX";
    
    /** 
     * PDOクラス
     * 
     * @var PDO $_pdo
     */
    private $_pdo = null;
    /** 
     * テスト対象テーブル名
     * 
     * @var string $_original_table
     */
    private $_original_table = null;
    /** 
     * テスト対象テーブルのバックアップ名
     * 
     * @var string $_backup_table
     */
    private $_backup_table = null;
    /** 
     * プライマリーキー情報を格納したリスト
     * 
     * @var string $_primaryKeyList 
     */
    private $_primaryKeyList = null;

    /**
     * コンストラクタ
     * 
     * @param string $original_table テスト対象テーブル名
     */
    public function __construct($original_table)
    {
        // プロパティ値保存
        $this->_original_table = $original_table;
        $this->_backup_table = "{$original_table}_backup";
    }

    /**
     * データベースをテストデータで初期化する
     * 
     * @param string $importXmlPath 初期データを格納したXML
     * 
     * @return void
     */
    public function init($importXmlPath)
    {        
        // DB接続
        if ($this->_pdo === null) {
            $option = array(PDO::MYSQL_ATTR_LOCAL_INFILE => true);
            $this->_pdo = new PDO(
                self::DSN, 
                self::USERNAME, 
                self::PASSWORD, 
                $option
            );
        }

        // テーブルバックアップ
        $this->_exec("DROP TABLE {$this->_backup_table}", false); // 念のため、削除
        $this->_exec(
            "CREATE TABLE {$this->_backup_table} LIKE {$this->_original_table}"
        );
        $this->_exec(
            "INSERT INTO {$this->_backup_table} 
                SELECT * FROM {$this->_original_table}"
        );

        // プライマリーキー情報を取得
        $this->_primaryKeyList = $this->_getPrimaryKeyList();

        // 初期データ投入
        $this->_loadXml($this->_original_table, $importXmlPath);
    }

    /**
     * データベースを元の状態に戻す
     * 
     * @return void
     */
    public function revert()
    {
        // テーブルを元に戻す
        $this->_exec("DROP TABLE {$this->_original_table}");
        $this->_exec(
            "ALTER TABLE {$this->_backup_table} RENAME TO {$this->_original_table}"
        );
    }

    /**
     * 実行後データ取得
     * 
     * @return array 実行後データを格納したリスト
     */
    public function getAfter()
    {
        // 実行後データを返却
        return $this->_selectAll($this->_original_table);
    }

    /**
     * 期待値データ取得
     * 
     * @param string $expectedXmlPath 期待値データを格納したXMLファイルパス
     * 
     * @return array 期待値データを格納したリスト
     */
    public function getExpected($expectedXmlPath)
    {
        // 比較データ投入
        $this->_loadXml($this->_original_table, $expectedXmlPath);

        // 比較データ取得
        return $this->_selectAll($this->_original_table);
    }

    /**
     * XMLデータを読み込み、DBテーブルに反映する
     * 
     * @param string $tableName   対象テーブル名
     * @param string $xmlFilePath XMLファイルパス
     * 
     * @return void
     */
    private function _loadXml($tableName, $xmlFilePath)
    {
        // XMLファイル存在チェック
        if (file_exists($xmlFilePath) === false) {
            throw new Exception("XMLファイルが存在しません: {$xmlFilePath}");
        }

        // XMLデータ投入
        $this->_exec("TRUNCATE TABLE {$tableName}");
        $this->_exec(
            "LOAD XML LOCAL INFILE '{$xmlFilePath}' INTO TABLE {$tableName}"
        );
    }

    /**
     * SQL実行 
     * 
     * @param string $sql     実行SQL
     * @param bool   $isCheck true:実行結果をチェックする、false:実行結果をチェックしない
     * 
     * @return void
     */
    private function _exec($sql, $isCheck = true)
    {
        $ret = (int)$this->_pdo->exec($sql);
        if ($isCheck === true && $ret === false) {
            throw new Exception("SQL実行に失敗しました: {$sql}");
        }
    }

    /**
     * SELECT実行
     * 
     * @param string $sql 実行SQL
     * 
     * @return array SQL実行結果を格納したリスト
     */
    private function _query($sql)
    {
        $stmt = $this->_pdo->query($sql);
        $ret = $stmt->fetchAll();

        if ($ret === false) {
            throw new Exception("SQL実行に失敗しました: {$sql}");
        }

        return $ret;
    }

    /**
     * プライマリーキー情報の取得
     * 
     * @return array プライマリーキー情報を格納したリスト
     */
    private function _getPrimaryKeyList()
    {
        preg_match('/dbname=(\w+)/', self::DSN, $match);
        $schema = $match[1];

        $sql = "
            SELECT
                column_name
            FROM
                information_schema.columns
            WHERE
                table_schema = '{$schema}'
                AND table_name   = '{$this->_original_table}'
                AND column_key   = 'PRI'
            ORDER BY
                ordinal_position
        ";
        return $this->_query($sql);
    }

    /**
     * 全レコード情報の取得
     * 
     * @param string $tabelName 対象テーブル名
     * 
     * @return array 全レコード情報を格納したリスト
     */
    private function _selectAll($tabelName)
    {
        $order = "";
        foreach ($this->_primaryKeyList as $primaryKey) {
            if ($order === "") {
                $order = $primaryKey[0];
            } else {
                $order = $order.", ".$primaryKey[0];
            }
        }
         
        $sql = "
            SELECT 
                *
            FROM 
                {$tabelName}
            ORDER BY
                {$order} ASC
        ";
        return $this->_query($sql);
    }
}
  • テストの実装例
<?php
require_once(dirname(__FILE__)."/DbTestUtil.php");

class DbTestSampleForPhpUnitOnly extends PHPUnit_Framework_TestCase
{
    /** DBを用いたユニットテストのヘルパークラス */
    private $_dbTestUtil = null;

    // 各テストメソッドの実行前に呼ばれる初期化処理
    public function setUp()
    {
        // テストデータの初期化
        $this->_dbTestUtil = new DbTestUtil("test");
        $this->_dbTestUtil->init(dirname(__FILE__)."/init/init.xml");
    }

    // 各テストメソッドの実行後に呼ばれる終了処理
    public function tearDown()
    {
        // テーブルを元に戻す
        $this->_dbTestUtil->revert();
    }

    // テストメソッド
    public function testSample()
    {
        // DB接続
        $dsn = "mysql:host=192.168.XX.XX;port=3306;dbname=XXXX;charset=utf8";
        $username = "XXXX";
        $password = "XXXX";
        $pdo = new PDO($dsn, $username, $password);
        
        // 参照系のテスト
        $stmt = $pdo->query("SELECT * FROM test ORDER BY id ASC");
        $list = $stmt->fetchAll();
        $answer = [
            ['id' => '1', 0 => '1', 'name' => 'john', 1 => 'john'],
            ['id' => '2', 0 => '2', 'name' => 'tom', 1 => 'tom'],
            ['id' => '3', 0 => '3', 'name' => 'apache', 1 => 'apache']
        ];
        $this->assertEquals($list, $answer);

        // 更新系のテスト
        $id = 4;
        $name = 'php';
        $stmt = $pdo->prepare("INSERT INTO test (id, name) VALUES (:id, :name)");
        $stmt->bindParam(':id', $id, PDO::PARAM_INT);
        $stmt->bindParam(':name', $name, PDO::PARAM_STR);
        $stmt->execute();
        // 実行結果比較
        $after = $this->_dbTestUtil->getAfter();
        $expected = $this->_dbTestUtil->getExpected(dirname(__FILE__)."/expected/expected.xml");
        $this->assertEquals($after, $expected);
    }
}