phpunitでprivateメソッドのテストを簡単に記述する

## 結論

`ReflectionWrapper` クラスを通すことで，可視性を気にすることなくメソッド/プロパティへアクセスできる．

```php
$testClass = new ReflectionWrapper(
    // テスト対象クラスのインスタンスを渡す
    new TestClass()
);

// private/protected なメソッドがそのまま呼び出せる
$testClass->doPrivate();

// private/protected なプロパティへもアクセス可能
$testClass->privateProperty = 'private';
```

`ReflectionWrapper` のコードはこちら

[https://github.com/ueba1110/utilities/tree/master/php/tests](https://github.com/ueba1110/utilities/tree/master/php/tests)


## 解説

### Reflectionクラスは記述が面倒

private/protected なメソッドをテストする場合， `ReflectionClass` で可視性を変更してアクセスする．

```php
$testClass = new TestClass();
$reflection = new \ReflectionClass(get_class($testClass));

// private/protected なメソッドを実行
$method = $reflection->getMethod('doPrivate');
$method->setAccessible(true);
$method->invoke($testClass);

// private/protected なプロパティへアクセス
$property = $reflection->getProperty('privateProperty');
$property->setAccessible(true);
$property->setValue($testClass, 'private');
```

ただ `ReflectionClass` による記述はテストに無関係であり，見通しが悪くなる．
できれば下記程度のコード量で記述したい．

```php
$testClass = new TestClass();

// private/protected なメソッドを実行
$testClass->doPrivate();

// private/protected なプロパティへアクセス
$testClass->privateProperty = 'private';
```


### ラッパークラスを作って解決する

`ReflectionClass` に関する処理をラッパークラスに集約することで，実際のテストコードでは可視性を気にすることなくアクセスできるようにする．

```php
<?php

declare(strict_types=1);

namespace Tests;

use ReflectionClass;

final class ReflectionWrapper
{
    /**
     * test対象クラスのインスタンス.
     *
     * @var mixed
     */
    private $instance;

    /**
     * test対象クラスのリフレクション.
     *
     * @var ReflectionClass
     */
    private ReflectionClass $reflection;

    /**
     * __construct.
     *
     * @param mixed $instance
     */
    public function __construct($instance)
    {
        $this->instance   = $instance;
        $this->reflection = new ReflectionClass($instance);
    }

    /**
     * __set.
     *
     * @param string $name
     * @param mixed  $value
     */
    public function __set(string $name, $value): void
    {
        $property = $this->reflection->getProperty($name);
        $property->setAccessible(true);
        $property->setValue($this->instance, $value);
    }

    /**
     * __get.
     *
     * @param string $name
     * @return mixed
     */
    public function __get(string $name)
    {
        $property = $this->reflection->getProperty($name);
        $property->setAccessible(true);

        return $property->getValue($this->instance);
    }

    /**
     * __call.
     *
     * @param string $name
     * @param array  $arguments
     * @return mixed
     */
    public function __call(string $name, array $arguments)
    {
        $method = $this->reflection->getMethod($name);
        $method->setAccessible(true);

        return $method->invokeArgs($this->instance, $arguments);
    }
}
```

上記 `ReflectionWrapper` を利用した場合のテストコードがこちら

```php
$testClass = new ReflectionWrapper(
    // テスト対象クラスのインスタンスを渡す
    new TestClass()
);

// private/protected なメソッドがそのまま呼び出せる
$testClass->doPrivate();

// private/protected なプロパティへもアクセス可能
$testClass->privateProperty = 'private';
```

ポイントはマジックメソッドの挙動である．
`__set` , `__get` , `__call` は，アクセス不能あるいは未定義のメソッド/プロパティへアクセスされた場合の挙動を， php標準から変更することが可能である．
`ReflectionWrapper` に対してメソッド呼び出しやプロパティアクセスを行うと，それらはいずれも未定義のため，各マジックメソッドが呼び出される．
各マジックメソッドでは，実際のテスト対象クラスのメソッド/プロパティを，可視性を変更した上で呼び出している．
結果，利用側ではまるで可視性を無視したような形でコードを記述できるようになる．

```php
$testClass = new ReflectionWrapper(
    // テスト対象クラスのインスタンスを渡す
    new TestClass()
);

// private/protected なメソッドがそのまま呼び出せる
$testClass->doPrivate();

// $testClass(ReflectionWrapper) は doPrivate メソッドが定義されていない
// => __call が実行される
// => __call 内で TestClass の doPrivate メソッドが，可視性を変更した上で実行される


// private/protected なプロパティへもアクセス可能
$testClass->privateProperty = 'private';

// $testClass(ReflectionWrapper) は privateProperty プロパティが定義されていない
// => __set が実行される
// => __set 内で TestClass の privateProperty プロパティに，可視性を変更した上で代入が行われる
```