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

単体テストはpublicで公開されているものだけで十分という意見が主流のようだ。
しかしprtectedメソッドにもサブクラスに「使ってもらう」意図がある場合など、むしろしっかりテストで固めておきたい場合もある。
prtectedで定義されたメソッドは外部から呼べないので、テストは難しい。

よくやる方法は、単体テスト用にターゲットクラスのサブクラスを作ることだ。
PHPではメソッドのオーバーライドするときに、アクセスレベルをprotectedからpublic に緩めることができるので、
そのなかで親クラスのprtectedメソッドをしれっと呼び直せばよい。

```php:
<?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:
<?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:
<?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:
<?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:
<?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](http://www.php.net/manual/ja/reflectionmethod.setaccessible.php)
[PHPUnitでProtectedなプロパティ、関数をテストする](http://qiita.com/items/9f7744e4ff1e65544f57)　,← 本件すでに投稿あり、参考にさせていただきました。
[php - Best practices to test protected methods with PHPUnit - Stack Overflow](http://stackoverflow.com/questions/249664/best-practices-to-test-protected-methods-with-phpunit)
[Testing protected Methods in Unit Tests : Frontalaufprall](http://www.frontalaufprall.com/2007/11/16/testing-protected-methods-in-unit-tests/)