LoginSignup
43
39

More than 5 years have passed since last update.

PHPUnitでprotectedメソッドのテストをしたい

Last updated at Posted at 2013-06-19

単体テストは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

43
39
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
43
39