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', '<'); } }