結論
ReflectionWrapper
クラスを通すことで,可視性を気にすることなくメソッド/プロパティへアクセスできる.
$testClass = new ReflectionWrapper(
// テスト対象クラスのインスタンスを渡す
new TestClass()
);
// private/protected なメソッドがそのまま呼び出せる
$testClass->doPrivate();
// private/protected なプロパティへもアクセス可能
$testClass->privateProperty = 'private';
ReflectionWrapper
のコードはこちら
https://github.com/ueba1110/utilities/tree/master/php/tests
解説
Reflectionクラスは記述が面倒
private/protected なメソッドをテストする場合, ReflectionClass
で可視性を変更してアクセスする.
$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
による記述はテストに無関係であり,見通しが悪くなる.
できれば下記程度のコード量で記述したい.
$testClass = new TestClass();
// private/protected なメソッドを実行
$testClass->doPrivate();
// private/protected なプロパティへアクセス
$testClass->privateProperty = 'private';
ラッパークラスを作って解決する
ReflectionClass
に関する処理をラッパークラスに集約することで,実際のテストコードでは可視性を気にすることなくアクセスできるようにする.
<?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
を利用した場合のテストコードがこちら
$testClass = new ReflectionWrapper(
// テスト対象クラスのインスタンスを渡す
new TestClass()
);
// private/protected なメソッドがそのまま呼び出せる
$testClass->doPrivate();
// private/protected なプロパティへもアクセス可能
$testClass->privateProperty = 'private';
ポイントはマジックメソッドの挙動である.
__set
, __get
, __call
は,アクセス不能あるいは未定義のメソッド/プロパティへアクセスされた場合の挙動を, php標準から変更することが可能である.
ReflectionWrapper
に対してメソッド呼び出しやプロパティアクセスを行うと,それらはいずれも未定義のため,各マジックメソッドが呼び出される.
各マジックメソッドでは,実際のテスト対象クラスのメソッド/プロパティを,可視性を変更した上で呼び出している.
結果,利用側ではまるで可視性を無視したような形でコードを記述できるようになる.
$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 プロパティに,可視性を変更した上で代入が行われる