PHP5.4時代のprivateメソッドテスト手法 #php5_4

Posted by Hiraku on 2011-12-07

PHP5.4 Advent Calendar 2011の7日目です。昨日は@madapajaさんの「PHP5.4+ で開発が行われている BEAR.Sunday フレームワークを動かしてみる」でした。

さてさて皆さん、ユニットテストしてますか? PHPもテストのライブラリが充実してきており、カバレッジ100%に情熱を燃やしている方も多いことでしょう。

ユニットテストで困るのが、private/protectedメソッドです。外から呼べないので、普通の方法ではテストできません。(protectedは適当なダミーのサブクラスを作ってそこからテストできますが、ちょっと面倒です。)

まあ当然です。外から呼べないようにprivateにしているので、簡単に呼べたら逆に困るわけです。しかしながら、privateメソッドも外から呼んでテストできた方が開発ははかどるでしょう。この記事ではPHPでprivateをテストする方法についてまとめていきます。

今まで

後述するReflectionMethod::setAccessibleが登場するPHP5.3.2以前では、privateの壁を突破する方法は"公式には"ありませんでした。

昔はrunkitという変態extensionを使うことでテストできたそうですが、このrunkitさん、最近のPHPバージョンに追随しておらず、微妙に使いにくい印象を受けます(PHP4っぽい雰囲気)。メソッドのアクセス権限を直接変更するような機能はないので、適当なダミークラスにメソッドをコピーしてテストする感じでしょうか?

他のprivateテスト方法としては、例えばC++の禁断の呪文#define private publicを真似する方法があります。requireの代わりにevalを使い、ソースコードを書き換えてしまえばいい。

<?php
//require_once 'Klass.php'; //requireの代わりに

eval('?'.'>'
  .str_replace(
    array('private','protected'),
    'public',
    file_get_contents('Klass.php')));

$obj = new Klass();
$obj->methodA(); //privateだったとしても呼べるはず

evalならprivateをpublicに置き換えてからコードを評価させることができます。あとは普通にpublicメソッドをテストする感覚でテストコードを書けます。

ただ、この方法は無理やりなのでいくつか問題があります。

  • クラス定義とテストコードが別ファイルになっている必要がある
  • 構文解析しない単純な置換なので、eval()でクラスを定義しているようなトリッキーなコードには適用できない。
  • コードに埋め込まれた文字列中の"private"も置き換わってしまう
  • requireした先で更にrequireしていた場合、置換されない。親クラスがprotectedのままなのにpublicでオーバーライドしようとするとエラーになって実行できない。

結局、適用できるコードは限られてしまいます。そんなわけで、privateメソッドのテストは諦める人も多かったのではないでしょうか? この状況はPHP5.3がリリースされてからもしばらくは変わりませんでした。

ReflectionMethod::setAccessible(PHP5.3.2以降)

そして、5.3.2でやっと、privateの公式な突破方法ができました。ReflectionMethod::setAccessibleを使うとprivateメソッドに外からアクセスできるようになります。

<?php
class Klass {
  private function methodA(){
    echo 'private methodA!',PHP_EOL;
  }
}

$obj = new Klass();

$methodA = new ReflectionMethod($obj, 'methodA');
$methodA->setAccessible(true);
$methodA->invoke(); //privateなはずのmethodAが呼べる!!

逐一、new ReflectionMethodしてsetAccessible(true)してと手間はかかりますが、こちらは公式手段なので先ほどのeval()による方法のデメリットは全て解消されています。

これでprivateメソッドのテスト問題は、一応の解決をみました。

PHP5.4のClosure::bind()

さて、ここで話は終わりません。やっとPHP5.4の話です。幸か不幸か、5.4でもう一つprivateを突破する方法が追加されています。それがClosure::bindです。

サラっと流されているような気もしますが、「無名関数に限り実行時コンテキストを動的に変更できる」という凄まじい機能です。「えっ? これ、できちゃっていいの?」と聞き返したくなるレベル。

Closure::bindを使うとこんなコードが実行できます。

<?php
class Klass {
  private function methodA(){
    echo 'private methodA!';
  }
}

Closure::bind(function(){

  var_dump($this);
  $obj = new Klass();
  $obj->methodA();

}, null, 'Klass')->__invoke();

//-----------------
//NULL
//private methodA!
//と出力される

クラスの中でないのに、methodA()が呼べていることがわかります。

Closure::bindは、第2引数に$thisにセットするオブジェクト、第3引数に実行時コンテキストを指定し、第1引数の無名関数のコピーを返します。…うーん、説明が難しいですね。

はしょって説明すると、このブロックに書かれたコードは、第3引数に指定したクラスの中で実行されるってところでしょうか。上記のコードでは"Klass"の中で実行されている(と見なされる)ため、Klassのprivateなメソッド、プロパティにアクセスできているのです。

さっきのReflectionMethodと比べてみると、リフレクションはメソッドごと・プロパティごとにnewしてsetAccessible(true)しなければいけませんでしたが、Closure::bindを使う場合はブロックを書くだけでいいので、とても読みやすいのではないでしょうか。

テストコードは"読みやすさ"が大変重視される分野です。DSLで文法をWrapしただけで別のライブラリを名乗っているものがあるぐらいです。まあ、それだけテストコードというのは汚れやすく、読みにくくなりやすいということでしょう。

もうちょっと具体的に、PHPUnit的なテストライブラリを使う場合だとこんなイメージになります。$thisって書いているのに$thisではなく$objのコンテキストで実行されているという、不思議な状況です。

<?php
class Klass {
  private function methodA(){
    return 'private methodA!';
  }
}

class KlassTest {
  public function testMethodA() {
    Closure::bind(function(){

      $obj = new Klass();
      $res = $obj->methodA();

      $this->assertTrue($res === 'private methodA!');

    }, $this, 'Klass')->__invoke();
  }

  public function assertTrue($bool) {
    echo ($bool) ? 'OK!' : 'NG!', PHP_EOL;
  }
}

$test = new KlassTest();
$test->testMethodA();

通常のpublic相手のテストコードと比較すると、違いは外側を覆っているブロックの存在だけです。これなら、ラッパーとか書かなくても充分これだけで使えると思います。

Closure::bindはevalやgotoと同じレベルで好き勝手できる危険な機能ですが、活用すると面白いと思います。PHP5.4が待ち遠しいですね。

明日は@do_akiさんです。

keyword: PHP AdventCalendar

PHPの最新記事