読者です 読者をやめる 読者になる 読者になる

takafumi blog

日々の勉強メモ

PHPUnitでprotected、privateのテスト

環境   PHP5.2 PHP5.3.2

PHPUnitでprotectedやprivateのメソッドをテストする方法についてまとめる。

php5.3.2以降ならReflectionを使うことができるので、意外と簡単にできる。
PHP: リフレクション - Manual

問題はそれ以前のバージョン。
普通の手法では対応できないので、強引に書き換える必要がある。
require_onceの代わりに直接ファイルを読み込み、アクセサを書き換えてclassをロードする。
・・・本当はPHP5.3.2未満は素直にバージョンアップするか、public以外のテストは諦めるほうがいいのかもしれないが、現場によっては結構5.1とかの事もあるので仕方がない。

以下は5.3.2未満と以上で自動で使い分けれるようにしたツール
メソッドだけでなく、プロパティも利用可能。

<?php
/**
 * php5.3.2未満、以上でprotected,privateをテストするための方法を切り分け
 *
 * PHPバージョン意識せず、以下のように使える。
 * ========================================================
 * require_once 'TestTool.php';
 * TestTool::import('Super.php');
 * TestTool::import('Hoge', true);
 * 
 * class HogeTest extends PHPUnit_Framework_TestCase
 * {
 *     const CLASS = 'Hoge';
 *     public $class = null
 * 
 *     }
 *     public function setUp()
 *     {
 *         $className = self::CLASS . TestTool::getSuffix();
 *         $this->class
 *                 = TestTool::on(new $className());
 *     }
 *     public function testFugaMethod()
 *     {
 *         // fooはprivate property
 *         $this->class->setProp('foo', 'Bar');
 *         $this->class->getProp('foo');
 *
 *         // piyoはprivate method
 *         // $this->class::__call()で処理される
 *         $this->class->piyo(); 
 *     }
 * }
 * ========================================================
 *
 */
class TestTool {
    const CLASS_SUFFIX = '_public';
    private $class;

    public function __construct($class) 
    {
        $this->class = $class;
    }

    public static function on($class) 
    {
        return new self($class);
    }

    public function __call($name, $args)
    {
        if (self::gePhpVer5_3_2()) {    // PHP_VERSION >= 5.3.2
            $method = new  ReflectionMethod($this->class, $name);
            $method->setAccessible(true);
            return $method->invokeArgs($this->class, $args);
        } else {
            return call_user_func_array(array($this->class, $name), $args);
        }
    }

    public function getProp($name)
    {
        if (self::gePhpVer5_3_2()) {    // PHP_VERSION >= 5.3.2
            $property = $this->getReflectionProperty($name);
            return $property->getValue($this->class);
        } else {
            return $this->class->$name;
        }
    }

    public function setProp($name, $val)
    {
        if (self::gePhpVer5_3_2()) {    // PHP_VERSION >= 5.3.2
            $property = $this->getReflectionProperty($name);
            $property->setValue($this->class, $val);
        } else {
            $this->class->$name = $val;
        }
    }

    /**
     * @phpversion >= 5.2.3
     * 
     *
     * accessibleをpublicにしたReflectionPropertyを返却
     */
    private function getReflectionProperty($name)
    {
        $refClass = new ReflectionClass($this->class); 
        if ($refClass->hasProperty($name)) {
            $property = $refClass->getProperty($name); 
        } else {
            $parentClass = $refClass->getParentClass();
            $property = $parentClass->getProperty($name);
            // 親クラスにない場合はException
        }
        $property->setAccessible(true);
        return $property;
    }

    public static function getSuffix()
    {
        if (self::ltPhpVer5_3_2()) {    // PHP_VERSION < 5.3.2
            return self::CLASS_SUFFIX;
        }
        return '';
    }

    public static function import($fileName, $extends = false)
    {
        if (self::gePhpVer5_3_2()) {    // PHP_VERSION >= 5.3.2
            require_once($fileName);
        } else {
            // PHP_VERSION < 5.3.2
            // ぶっちゃけphp5.3.2未満ではprotectedとかprivateのテストは止めるべきかも
            // かなり強引。
            // public置換時に構文を最低限しか解析していないので注意
            self::evalRequire($fileName, $extends);
        }
    }

    /**
     * @phpversion < 5.2.3
     */
    private static function evalRequire($fileName, $extends)
    {
        $contents = file_get_contents($fileName);
        $contents = self::replacePropertyToPublic($contents);
        $contents = self::replaceFunctionToPublic($contents);
        $contents = self::replaceClassName($contents);
        if ($extends) {
            $contents = self::replaceExtendsClassName($contents);
        } 
        eval('?' . '> ' . $contents); // ?と>を'.'で連結しているのは、しないとviでsyntax途切れるから
    }

    /**
     * @phpversion < 5.2.3
     */
    private static function replaceExtendsClassName($contents)
    {
        return preg_replace(
                    '/extends +([^ \n\{]+)/',
                    sprintf('extends \1%s', self::CLASS_SUFFIX),
                    $contents );
    }

    /**
     * @phpversion < 5.2.3
     * 
     * 同じファイル読み込むとエラーになるのでsuffixをつけて読み込む
     */
    private static function replaceClassName($contents)
    {
        return preg_replace(
                    '/class +([^ ]+)/',
                    sprintf('class \1%s', self::CLASS_SUFFIX),
                    $contents );
    }

    /**
     * @phpversion < 5.2.3
     */
    private static function replaceFunctionToPublic($contents)
    {
        return preg_replace(
                    '/private +function|protected +function/',
                    'public function',
                    $contents );
    }

    /**
     * @phpversion < 5.2.3
     */
    private static function replacePropertyToPublic($contents)
    {
        return preg_replace(
                    '/private +\$|protected +\$/',
                    'public \$',
                    $contents );
    }

    public static function gePhpVer5_3_2()    // ge = greater or equal
    {
        return version_compare(PHP_VERSION, '5.3.2', '>=');
    }

    public static function ltPhpVer5_3_2()    // lt = less than 
    {
        return version_compare(PHP_VERSION, '5.3.2', '<');
    }
}