プライベートメソッドに対するテスト方法
phpunit などを使って PHP スクリプトをテストする時に、プライベートメソッドをテストしたくなる時があると思いますが、PHP では結構難しいように思います。
PHP でプライベートメソッドをテストする方法として、思い付くのは以下の方法くらいでしょうか。
- パブリックメソッドからプライベートメソッドを完全にテストできるように工夫する
- プライベートメソッドのテスト用にパブリックメソッドのラッパーを作成しておく
- テスト時のみ、テストしたいプライベートメソッドの Private を Public に書き換える
- 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 ) );