private,protectedなメソッドを呼び出してテストする(新)

4年ほど前に書いた記事[^1]の内容を書き直したもの。

## 問題

ユニットテストで`private`や`public`なメソッドをテストしたいとします。

@t_wadaさんが[プライベートメソッドのユニットテストは書かないもの？](https://qa.atmarkit.co.jp/q/2784)に書いてる通りデメリットがある行為だと把握しつつ、ここでは「書く」ことにしよう。

## 使用方法

今回は雑にテストを書いていく。

```php
<?php

class Hoge
{
    public static function foo() { return __METHOD__; }
    private static function bar() { return __METHOD__; }
}

assert('Hoge::foo' === Hoge::foo());
assert('Hoge::bar' === Hoge::foo()); // ここで Errorが発生
// Call to private method Hoge::bar()

echo 'ok', PHP_EOL;
```

ここで、このあと紹介する`PrivateCaller`を使って、こうする。

```php
assert('Hoge::foo' === Hoge::foo());
assert('Hoge::bar' === PrivateCaller::invokeMethod("Hoge::bar"));

echo 'ok', PHP_EOL;
```

これでエラーが出ずに実行できた。やりましたね。！

引数をとる関数の場合は以下のようにします。

```php
$actual = PrivateCaller::invokeMethod("Cat::nyan", 'arg1', 'arg2', 'arg3');

$args = ['arg1', 'arg2', 'arg3'];
$actual = PrivateCaller::invokeMethodArgs("Cat::nyan", $args);
```

`PrivateCaller::invokeMethod("Cat::nyan", ...$args)`とも書けそうですが、これは別の機能です。
`PrivateCaller::invokeMethodArgs("Cat::nyan", ['arg1', 'arg2', 'arg3'])` これは動きません。

## 実装

適当にコピペしてください。

```PrivateCaller.php
<?php /* tadsan@pixiv.com | license WTFPL */

trait PrivateCaller
{
    /**
     * アクセス権限のないメソッドを起動する
     *
     * @param callable|string $class_method
     * @phan-param array{0:object|string,1:string}|string
     */
    public static function invokeMethod($class_method, ...$args)
    {
        if (\is_array($class_method)) {
            [$class, $method] = $class_method;
        } elseif (\is_string($class_method) && \strpos($class_method, '::') !== false) {
            [$class, $method] = \explode('::', $class_method, 2);
        } else {
            throw new \DomainException('$class_method is not method');
        }

        $ref = new \ReflectionMethod($class, $method);
        $ref->setAccessible(true);

        return $ref->invokeArgs(is_object($class) ? $class : null, $args);
    }

    /**
     * アクセス権限のないメソッドを配列変数のリファレンスを渡して起動する
     *
     * @param callable|string $class_method
     * @phan-param array{0:object|string,1:string}|string
     */
    public static function invokeMethodArgs($class_method, array &$args)
    {
        if (\is_array($class_method)) {
            [$class, $method] = $class_method;
        } elseif (\is_string($class_method) && \strpos($class_method, '::') !== false) {
            [$class, $method] = \explode('::', $class_method, 2);
        } else {
            throw new \DomainException('$class_method is not method');
        }

        $ref = new \ReflectionMethod($class, $method);
        $ref->setAccessible(true);

        return $ref->invokeArgs(is_object($class) ? $class : null, $args);
    }

    /**
     * アクセス権限のないメソッドを起動できるクロージャを返す
     *
     * @param callable|string $class_method
     * @phan-param array{0:object|string,1:string}|string
     */
    public static function getCallable($class_method): \Closure
    {
        if (\is_array($class_method)) {
            [$class, $method] = $class_method;
        } elseif (\is_string($class_method) && \strpos($class_method, '::') !== false) {
            [$class, $method] = \explode('::', $class_method, 2);
        } else {
            throw new \DomainException('$class_method is not method');
        }

        $ref = new \ReflectionMethod($class, $method);
        $obj = is_object($class) ? $class : null;
        $ref->setAccessible(true);

        return function (array &$args = []) use ($ref, $obj) {
            return $ref->invokeArgs($obj, $args);
        };
    }
}
```

### なぜ`invokeMethod`と`invokeMethodArgs`が分かれてるの？

可変長引数ではリファレンスとの相互運用に難があるからです。

以下のようにリファレンスを使った機能は`PrivateCaller::invokeMethod()`では呼べません。

```Preg.php
class Preg
{
    private static function match(string $pattern, string $subject, array &$matches = null, int $flags = 0, int $offset = 0)
    {
        return preg_match($pattern, $subject, $matches, $flags, $offset);
    }
}

// これはだめ (リファレンスが通らない)PrivateCaller::invokeMethod('Preg::match', $pattern, $subject, $n);

// これなら呼べる
$args = ['/\A#(.*)#\z/', '#xxxx#'];
$m = null;
$args[2] = &$m;
PrivateCaller::invokeMethodArgs('Preg::match', $args);

// var_dump($m);
// array(2) {
//   [0]=>
//   string(6) "#xxxx#"
//   [1]=>
//   string(4) "xxxx"
// }
```

どうしてこれなら動くのか知りたいひとは[PHPのリファレンス(参照&)の傾向と対策、あるいはさよなら - Qiita](https://qiita.com/tadsan/items/74f992dcc48216b571bd)を読んでください。

さらについでになのですが、リファレンスをとるために以下のように仕様を変更することは可能です。

```php
public static function invokeMethod($class_method, &...$args)
```

↑このように仕様変更すると、どのような影響が出るかは読者への課題とします。

## PHPUnitから使用したい場合

トレイトなので、クラスから利用できます。これは好みの問題ですが個別のテストケースでは`\PHPUnit\Framework\TestCase`は直接継承せず、プロジェクトごと、あるいはテストの文脈ごとに共通の`abstract class`を用意するのがおすすめです。

その共通クラスでべんりユーティリティをくっつけておくかどうかはまた議論が分かれるところですが、私はユーティリティも個別のテストケースクラスでは追加せず、共通クラスで`use`しておいた方が楽でいいよね派です。

さて、そのような価値観で書くとこうなります。

```TestCase.php
<?php

namespace YourApp;

abstract class TestCase extends \PHPUnit\Framework\TestCase
{
    use PrivateCaller;

    // そのほかの処理を書く
}
```

```HogeTest.php
<?php

namespace YourApp;

abstract class TestCase extends TestCase
{
     private $hoge;

     public function setup()
     {
         $this->hoge = new Hoge();
     }

     public function test()
     {
         $actual = $this->invokeMethod([$this->hoge, 'piyo'], 'arg1', 'arg2');
         $this->assertEquals('expected', $actual);
     }
}
```

`invokeMethod`なんて長ったらしい？ それなら`use`の部分をちょっと直します。

```TestCase.php
abstract class TestCase extends \PHPUnit\Framework\TestCase
{
    use PrivateCaller {
        invokeMethod as call;
        invokeMethodArgs as callA;
    }
}
```

`call`とか`callA`とか書いてる部分は好きな名前に直してください。
[トレイト](http://php.net/manual/ja/language.oop5.traits.php)って最高便利ですよね？

## あとがき

みなさまはトレイトをクラスで`use`しなくてもそのまま呼び出せるって知ってましたか？

## おまけ

なぜ今回のコードでは型宣言に`callable`と書かなかったのかは読者への課題ry

## 脚注

[^1]: [private,protectedなメソッドを呼び出してテストする - Qiita](https://qiita.com/tadsan/items/4800e5635d76c3542d95)