プライベートメソッドに対するテスト方法

phpunit などを使って PHP スクリプトをテストする時に、プライベートメソッドをテストしたくなる時があると思いますが、PHP では結構難しいように思います。
PHP でプライベートメソッドをテストする方法として、思い付くのは以下の方法くらいでしょうか。

  1. パブリックメソッドからプライベートメソッドを完全にテストできるように工夫する
  2. プライベートメソッドのテスト用にパブリックメソッドのラッパーを作成しておく
  3. テスト時のみ、テストしたいプライベートメソッドの Private を Public に書き換える
  4. Runkit などを使って定義を変更する

1. が実現できれば問題ないのですが、全てがうまくいくようにパブリックメソッドを作成するのは困難だと思います。また、テストのためだけにメソッドを増やしたり、変更したくないため、2. と 3. はあまりやりたくありません。4. であればできそうな気がしたのですが、Runkit では、定義済みのメソッドのアクセス定義のみを変更するようなことはできないようで、要件を満たすのは難しそうでした。

Java では、AccessibleObject を使用することで、フィールドやメソッドに対するアクセス制御ができます。PHP でも、PHP 5.3.0 から、ReflectionProperty クラスに setAccessible() が追加され、プロパティ(フィールド)へのアクセス制御ができるようになったのですが、ReflectionMethod クラスには、setAccessible() は実装されていません。
そこで、ReflectionMethod クラスに setAccessible() を実装してみることにしました。

PHP に対する Patch

以下のような Patch で実現することができました。PHP 5.3.0 に対する Patch です。PHP 5.2.10 以前のバージョンに適用できるかは確認していません。

diff -ur php-5.3.0.orig/ext/reflection/php_reflection.c php-5.3.0/ext/reflection/php_reflection.c
--- php-5.3.0.orig/ext/reflection/php_reflection.c	2009-06-16 23:33:33.000000000 +0900
+++ php-5.3.0/ext/reflection/php_reflection.c	2009-08-08 22:25:46.000000000 +0900
@@ -2945,6 +2945,28 @@
 }
 /* }}} */
 
+/* {{{ proto public int ReflectionMethod::setAccessible()
+   Sets whether non-public method can be requested */
+ZEND_METHOD(reflection_method, setAccessible)
+{
+	reflection_object *intern;
+	zend_function *mptr;
+	zend_bool accessible;
+
+	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "b", &accessible) == FAILURE) {
+		return;
+	}
+	METHOD_NOTSTATIC(reflection_method_ptr);
+	GET_REFLECTION_OBJECT_PTR(mptr);
+
+	if (accessible) {
+		mptr->common.fn_flags |= ZEND_ACC_PUBLIC;
+	} else {
+		mptr->common.fn_flags &= ~ZEND_ACC_PUBLIC;
+	}
+}
+/* }}} */
+
 /* {{{ proto public static mixed ReflectionClass::export(mixed argument [, bool return]) throws ReflectionException
    Exports a reflection object. Returns the output if TRUE is specified for return, printing it otherwise. */
 ZEND_METHOD(reflection_class, export)
@@ -5072,6 +5094,10 @@
 	ZEND_ARG_ARRAY_INFO(0, args, 0)
 ZEND_END_ARG_INFO()
 
+ZEND_BEGIN_ARG_INFO(arginfo_reflection_method_setAccessible, 0)
+	ZEND_ARG_INFO(0, value)
+ZEND_END_ARG_INFO()
+
 static const zend_function_entry reflection_method_functions[] = {
 	ZEND_ME(reflection_method, export, arginfo_reflection_method_export, ZEND_ACC_STATIC|ZEND_ACC_PUBLIC)
 	ZEND_ME(reflection_method, __construct, arginfo_reflection_method___construct, 0)
@@ -5089,6 +5115,7 @@
 	ZEND_ME(reflection_method, invokeArgs, arginfo_reflection_method_invokeArgs, 0)
 	ZEND_ME(reflection_method, getDeclaringClass, NULL, 0)
 	ZEND_ME(reflection_method, getPrototype, NULL, 0)
+	ZEND_ME(reflection_method, setAccessible, arginfo_reflection_method_setAccessible, 0)
 	{NULL, NULL, NULL}
 };

PHP のテストコード

以下、PHP のテストコードです。

<?php

class Test
{
    public    function test1( $args ) { echo 'Public   :' . implode( ",", $args ). "\n"; }
    protected function test2( $args ) { echo 'Protected:' . implode( ",", $args ). "\n"; }
    private   function test3( $args ) { echo 'Private  :' . implode( ",", $args ). "\n"; }
}

function test( $method, $obj, $args )
{
    try {
        $method->invoke( $obj, $args );
    }
    catch ( Exception $e ) {
        echo "Error\n";
    }
}

$test = new Test();
$method1 = new ReflectionMethod( 'Test', 'test1' );
$method2 = new ReflectionMethod( 'Test', 'test2' );
$method3 = new ReflectionMethod( 'Test', 'test3' );
test( $method1, $test, array( 1, 2, 3 ) );
test( $method2, $test, array( 4, 5, 6 ) );
test( $method3, $test, array( 7, 8, 9 ) );

$method2->setAccessible( TRUE );
$method3->setAccessible( TRUE );
test( $method1, $test, array( 1, 2, 3 ) );
test( $method2, $test, array( 4, 5, 6 ) );
test( $method3, $test, array( 7, 8, 9 ) );

$method2->setAccessible( FALSE );
$method3->setAccessible( FALSE );
test( $method1, $test, array( 1, 2, 3 ) );
test( $method2, $test, array( 4, 5, 6 ) );
test( $method3, $test, array( 7, 8, 9 ) );

結果

Public   :1,2,3
Error
Error
Public   :1,2,3
Protected:4,5,6
Private  :7,8,9
Public   :1,2,3
Error
Error

以上で、ReflectionMethod クラスの setAccessible() を使ってプライベートメソッドへのアクセス制御ができるようになりました。これで、PHP スクリプトを修正せずに、プライベートメソッドのテストができそうです。
とりあえず、当初の目的はこれで達成できそうな気がするのですが、他の方がプライベートメソッドのテストをどのようにしているのか気になります。