久しぶりに技術ネタができたので投稿します。
最近、社内システムチームでも「品質向上のために自動テストを導入していこうぜ」という流れになってきました。
その中でprivateとprotectedのメソッドをpublicのものであるかのようにアクセスするためのクラスを用意しました。
結構使いやすい代物になったと思いますので、ちょっと書いておこうと思います。
どうしてこんなことをやろうとしたのか?
実は今まで社内システムには自動テストがほとんどありませんでした。
いまはだいたいの機能を把握しているので何とかなっていますが、今後機能が増えていくときにどう考えても影響範囲がわからなくなって辛くなっていく未来が見えています。
そこで少しずつでもphpunitを使って自動テストを作っていくことになりました。
手始めにいま作っているところから順に作っていこうと思っていたのですが、私の思想的なアレでいろんな処理をprivateやprotectedなメソッドにお願いするような作り方をしているので、それらのメソッドをテストする必要に迫られました。
何が問題なの?
privateとprotectedなメソッドは通常の方法では外から呼べないため、phpunitなどでテストをする際に結構面倒です。
※ 以下の説明では以下のクラスをテストする場合を想定します
class YourTestTargetClass{
private function somePrivateMethod($some_data)
{
return $some_data !== '';
}
}
ちょっと調べるとこんな感じでやれば大丈夫だよ!という情報が見つかります。
$target = new YourTestTargetClass();
// ここからprivate/protectedなメソッドを呼べるようにするおまじない
$reflection = new \ReflectionClass('YourTestTargetClass');
$method = $reflection->getMethod('somePrivateMethod');
$method->setAccessible(true);
$argument = 'hogehoge';
// privateなメソッドを呼ぶ
echo $method->invokeArgs($target, [$argument]);
こんなの毎回やってられないなぁというのが正直な感想でした。
というわけで楽にやる子を作ってみました
このクラスを使うとアクセス制限を無視してアクセスできるようになります。
意図しない使い方が増えることで保守要因から死人が出る可能性が高くなるので、使うのはテストの時くらいにしておくことを強くオススメします。
privateなメソッドもテストするクラス
class MethodTester
{
/** @var object テスト処理を実行するクラスのインスタンス */
protected $___target_instance;
/** @var object ReflectionClassのオブジェクト */
protected $___reflect_obj;
/**
* MethodTester constructor.
*
* @param object $target_instance private, protectedメソッドを呼べるようにするインスタンス
* @throws \ReflectionException
*/
public function __construct($target_instance)
{
$this->___target_instance = $target_instance;
$this->___reflect_obj = new \ReflectionClass($target_instance);
}
public function __get($name)
{
$property = $this->___reflect_obj->getProperty($name);
$property->setAccessible(true);
return $property->getValue($this->___target_instance);
}
public function __set($name, $value)
{
$property = $this->___reflect_obj->getProperty($name);
$property->setAccessible(true);
$property->setValue($this->___target_instance, $value);
}
public function __call($name, $arguments)
{
$method = $this->___reflect_obj->getMethod($name);
$method->setAccessible(true);
return $method->invokeArgs($this->___target_instance, $arguments);
}
}
使い方
先ほどのYourTestTargetClassクラスを何も考えずにテストしようとすると、当然実行時にエラーが発生します1)辛いですね。
// 普通にインスタンスを生成してテストしようとしてみる
$target = new YourTestTargetClass();
$correct_data = 'hogehoge';
// 外からprivateメソッドにアクセスしようとしているのでエラー発生
echo $target->somePrivateMethod($correct_data);
そこで今回作成したMethodTesterを使ってみます。
下のように使うことでprivateとprotectedメソッドを呼び出すことができるようになります。
// MethodTester のコンストラクタにprivateメソッドのテストをしたいクラスのインスタンスを渡します。
$target = new MethodTester(new YourTestTargetClass());
$correct_data = 'hogehoge';
// $targetがYourTestTargetClassクラスのインスタンスであるかのように使うと
// privateメソッドがpublicメソッドであるかのように呼べます
echo $target->somePrivateMethod($correct_data);
phpunitでテストする場合には以下のように使えば大丈夫なハズです
class YourTestTargetClassTest extends TestCase
{
public function test_somePrivateMethod()
{
$target = new MethodTester(new YourTestTargetClass());
$this->assertTrue($target->somePrivateMethod('hogehoge'));
$this->assertNotTrue($target->somePrivateMethod(''));
}
}
ここにこだわって作りました(ドヤァ
ちょっと調べると同様のクラスはたくさん見つかります。
それでもわざわざ自作したのは、かなり個人的な趣味のためです。
私はこういう便利クラスを使う時に「いかにもアダプタっぽいクラスを使ってますよー」という使い方をしなければいけないつくりはあまり好きではありません。
なのでアダプタっぽいクラスを意識しないで使えるようにしてみました。
ちなみにこっそりとprivate/protectedなメンバ変数についても外からアクセスできるように作っています。
動きを簡単に解説します
ものすごく簡単に説明すると、メソッドコールのマジックメソッドをオーバーライドして、クラス外から対象メソッドにアクセスできるように設定してからメソッドを実行しているだけです。2)メンバ変数へのアクセスも同様です
もうちょっとちゃんとした解説
以下技術的な話になりますので、興味がない方は読み飛ばしてください。
主役はアウトローなReflectionClass
今回の主役は ReflectionClass ちゃんです。(PHP.netのReflectionClassページ)
この子はクラスの構造をごにょごにょできるアウトローなクラスです。3)個人的にはcのvoidポインタくらいアウトローだと思います。
ReflectionClassのgetMethodを使えばメソッドのアクセス制限を変更した後にメソッドを呼び出すことができます。
普通にプログラムを組んでいるときにはほぼお目にかかることはないクラスですね。4)私は幸運にも今まで見たことがありませんでした
もう一人の主役はマジックメソッド
MethodTester でオーバーライドしている __call や __get はphpがサポートしているマジックメソッドというものです。(php.netのマジックメソッドページ)
PHP.netさんいわく
__call() は、 アクセス不能メソッドをオブジェクトのコンテキストで実行したときに起動します。
とのことです。
MethodTester では独自のメソッドを宣言していないので、コンストラクタで渡したインスタンスに属しているメソッドを呼ぶ場合に毎回呼ばれることになります。
なので、__callの中でアクセス制限を外してから対象メソッドを呼ぶようにしています。
そうすれば外からは普通にメソッドを呼ぶだけで、privateやprotectedのアクセス制限を無視してメソッドを呼べるようになるわけです。5)チェックするのが面倒なのでpublicなメソッドを呼ぶ場合もアクセス制限を変更しています。
おわりに
staticなメソッドはオーバーライドとかでは回避できないので、今回は対応を見送りました。いい方法ありそうなんですけどね。
これを使えばprivate/protectedメソッドのmockも作れるようになるだろうと思っているので、時間を見つけてやってみようと思います。6)いまは公式で用意している方法ではできないんですよねぇ・・・
それもうまくいったらphpunitのgithubに入れてもらうようissue切ったりして動いてみようかなぁなんて妄想をしています。
とりあえずは英語でissue書くのが一番のハードルです。
最後まで読んでいただきありがとうございました。