単体テストはpublicで公開されているものだけで十分という意見が主流のようだ。
しかしprtectedメソッドにもサブクラスに「使ってもらう」意図がある場合など、むしろしっかりテストで固めておきたい場合もある。
prtectedで定義されたメソッドは外部から呼べないので、テストは難しい。
よくやる方法は、単体テスト用にターゲットクラスのサブクラスを作ることだ。
PHPではメソッドのオーバーライドするときに、アクセスレベルをprotectedからpublic に緩めることができるので、
そのなかで親クラスのprtectedメソッドをしれっと呼び直せばよい。
<?php
class Target {
protected function getRealName() {
return __METHOD__;
}
}
?>
<?php
class TargetExp extends Target {
public function getRealName() {
return parent::getRealName();
}
}
class TargetClass extends PHPUnit_Framework_TestCase{
function testGetRealName() {
$target = new TargetExp;
$this->assertEquals("Target::getRealName", $target->getRealName());
}
}
オーバーライドも数が多いといちいち書くのもダルくなる。
そこで__call()を使えば一括で処理できてコピペするにもやさしい。
<?php
class TargetExp extends Target {
public function __call($name, $args) {
return call_user_func_array( array($this, $name), $args);
}
}
まあ、コピペとはいえ個々のサブクラス定義を追記すること自体、気に入らないひともいるだろう。
ところで、PHP 5,3,2 からリフレクションに追加されたsetAccessible()というのがあって、private/protected なメソッドのアクセス制御を無力化できるようになったようだ。
それができるならサブクラスを定義するまでもなく、もっと汎用的なプロキシクラスを作れる。
クラスの型を見ないテストコードならこれで問題ないはずだ。
<?php
class chop {
private $target;
private function __construct($target) {
$this->target = $target;
}
public static function on($target) {
return new self($target);
}
public function __call($name, $args) {
$method = new ReflectionMethod($this->target, $name);
$method->setAccessible(true);
return $method->invokeArgs($this->target, $args);
}
}
// 使用例
$target = chop::on( new Target() );
$actual = $target->getRealName();
上記コードをPHPUnit の bootstarpにコピペしておくだけで、テストケースごとにサブクラスを定義する必要もない。
問題は setAccessible() がPHP 5.3.2からしかサポートされないことだ。
現実問題としてもっと古いPHP 環境で稼働しているサービスは多く、
個人的にも開発現場でPHP 5.3 以上にお目にかかったことはない。(オレだけ?)
結局サブクラス化で対応することにしたのだが、もう少し楽をするためサブクラスを生成する関数を書いた。
少し乱暴だがeval()でサブクラスの宣言をしてしまう。
これをPHPUnit の bootstarpに置くと、テストケースの任意の場所で使用できる。
<?php
function unprotect($class, $suffix="Unprotected") {
if (!class_exists($class))
trigger_error("Bad class : {$class}", E_USER_ERROR);
if (class_exists($class.$suffix))
return $class.$suffix;
if (method_exists('ReflectionMethod', 'setAccessible')) {
$def_class = <<< CLS
class {$class}{$suffix} extends {$class} {
public function __call(\$name, \$args) {
\$method = new ReflectionMethod(\$this, \$name);
\$method->setAccessible(true);
return \$method->invokeArgs(\$this, \$args);
}
public static function __callStatic(\$name, \$args) {
\$method = new ReflectionMethod("{$class}", \$name);
\$method->setAccessible(true);
return \$method->invokeArgs(null, \$args);
}
}
CLS;
eval($def_class);
} else {
$def_class = <<< CLS2
class {$class}{$suffix} extends {$class} {
public function __call(\$name, \$args) {
if (!in_array(\$name, get_class_methods('{$class}')))
trigger_error("Bad method : {$class}::{\$name}", E_USER_ERROR);
return call_user_func_array(array(\$this, \$name), \$args);
}
}
CLS2;
eval($def_class);
}
return $class.$suffix;
}
使用例
<?php
// サブクラス名を気にしない
$class_name = unprotect("Target");
$target = new $class_name(); // new TargetUnprotected();
// クラス名サフィックスを指定
unprotect("Target", "Exp");
$target = new TargetExp();
// protectedメソッドの呼び出しが可能。
// PHP 5.3.2 以上ならprivateでさえ呼べてしまう。
$target->getRealName();
// static メソッドも呼べる。
// PHP 5.3.2 以上の場合
$class_name::doStatic($a $b);
TargetExp::doStatic($a $b);
// もっと古いPHPの場合
$target->doStatic($a,$b);
参考
PHP: ReflectionMethod::setAccessible - Manual
PHPUnitでProtectedなプロパティ、関数をテストする ,← 本件すでに投稿あり、参考にさせていただきました。
php - Best practices to test protected methods with PHPUnit - Stack Overflow
Testing protected Methods in Unit Tests : Frontalaufprall