Do You PHP はてブロ

Do You PHPはてなからはてブロに移動しました

test_helpers拡張モジュール

気がついたら、PHPUnitの作者であるSebastian Bergmann氏が"test_helpers"なる拡張モジュールを公開していたようです。

コンセプトとしては、ユニットテストを書けないようなレガシーコードで、ユニットテストを楽に書けるようにするための拡張モジュールのようで、ざっと機能をまとめてみると、

  • exit/die関数の無効化やコールバックを指定できる
  • newオペレータの実行時にコールバックを指定できる
  • 関数の改名

な感じです。感覚的には、PECL :: Package :: runkitに近い感じです。
とりあえず、README.markdownを訳してみましたので晒しておきます。間違いがあれば指摘してください;-)
あと、サンプルコードはPHP5.3系のようですが、typoがあるので注意してください。ちなみに、PHP5.2.14でも動作しましたが、手元の環境では

php: symbol lookup error: /path/to/test_helpers.so: undefined symbol: zend_fcall_info_argn

が出ています。Issue Trackに上げとくかなぁ。。。
おまけ。PHP5.3向けWindows版dllもあります。

ext/test_helpers

`ext/test_helpers` は、PHPコードのテストを用意にするためのPHPインタプリタ向け拡張モジュールです。

インストール

`ext/test_helpers` は、[PEAR Installer](http://pear.php.net/) でインストールします。 このインストーラPEAR,PECLのバックボーンであり、PHPパッケージと拡張モジュールの分配システムを提供します。

`ext/test_helpers` を分散するのに使われる PEAR チャネル (`pear.phpunit.de`) が、ローカルのPEAR環境で登録されている必要があります:

sb@ubuntu ~ % pear channel-discover pear.phpunit.de
Adding Channel "pear.phpunit.de" succeeded
Discovery of channel "pear.phpunit.de" succeeded

これはただ一度だけ実行します。こうすることで、PEAR インストーラを使って、PHPUnitチャネルから拡張モジュールやパッケージをインストールできるようになります:

sb@ubuntu ~ % pecl install phpunit/test_helpers
downloading test_helpers-1.0.0.tgz ...
Starting to download test_helpers-1.0.0.tgz (6,980 bytes)
.....done: 6,980 bytes
4 source files, building
.
.
.
install ok: channel://pear.phpunit.de/test_helpers-1.0.0
You should add "extension=test_helpers.so" to php.ini

PHP用のスタンドアローンな拡張モジュールのビルドについてのその他詳細は、PHPマニュアルの [Installation of PECL extensions](http://php.net/install.pecl) を参照してください。

使用方法

exit ステートメントインターセプト

[ユニットテスト](http://en.wikipedia.org/wiki/Unit_test) が `exit` や `die` ステートメントを含むコードを実行する場合、すべてのテストスイートの実行が停止されます。これは、良いことではありません。
`set_exit_overload()` 関数を使用することで、`exit` / `die` ステートメントオーバーロードが可能になり、何もしないようにできます。たとえば次のような感じです:

<?php
set_exit_overload(function() { return FALSE; }
exit;
print 'We did not exit.';
unset_exit_overload();
exit;
print 'We exited and this will not be printed.';
?>

上記のコードは以下を出力します。

We did not exit.

`set_exit_overload()` によって登録されたコールバックは、`exit` / `die` が呼び出されたときのパラメータを受け取ります:

<?php
set_exit_overload(function($param = NULL) { echo ($param ?: "No value given"), "\n"; return FALSE; }
die("Hello");
die;
?>

上記のコードは以下を出力します。

Hello
No value given

`die()` や `exit`などの低レベル関数やステートメントを処理する他の方法は、デフォルト(本稼動時)では本来の実装に処理を委譲するが、テスト時にはテスト可能なような振る舞いをするプロキシでそれらをトラップすることです。

オブジェクト生成のインターセプト

[ユニットテスト](http://en.wikipedia.org/wiki/Unit_test)では、[モックオブジェクト](http://en.wikipedia.org/wiki/Mock_Object) を利用することで、複雑で本当の(モックではない)オブジェクトの振る舞いをシミュレートできますので、実オブジェクトをユニットテスト内で利用することが困難もしくは不可能な場合に役に立ちます。
モックオブジェクトは、プログラムがモッククラスのオブジェクトを想定している箇所であれば、プログラム内のどこでも利用できます。しかし、そのオブジェクトが、オリジナルのオブジェクトが利用されるコンテキスト内に渡される場合に限ります。
次のような例を考えてみましょう:

<?php
class SomeClass
{
    public function doSomething()
    {
        $object = new SomeOtherClass;
        // ...
    }
}
?>

上記のコードは、`SomeOtherClass` クラスのオブジェクトを生成せずに `SomeClass::doSomething()` メソッド用のユニットテストを実行することは不可能です。このメソッドはそれ自身で `SomeOtherClass` のオブジェクトを生成するので、代わりにモックオブジェクトを注入することはできません。
理想の世界では、上記のようなコードは [依存性の注入](http://en.wikipedia.org/wiki/Dependency_Injection) を用いてリファクタリングされます:

<?php
class SomeClass
{
    protected $object;

    public function __construct(SomeOtherClass $object)
    {
        $this->object = $object;
    }

    public function doSomething()
    {
        // ...
    }
}
?>

残念なことに、これはいつも可能ではありません (とは言っても、技術的な理由によるものではありません)。
これは、`set_new_overload()` 関数が有効な場面です。`new` オペレータが実行されたとき、自動的に呼び出される [コールバック](http://www.php.net/manual/en/language.pseudo-types.php) を登録できます。

<?php
class Foo {}
class Bar {}

function callback($className) {
    if ($className == 'Foo') {
        $className = 'Bar';
    }

    return $className;
}

var_dump(get_class(new Foo));

set_new_overload('callback');
var_dump(get_class(new Foo));
?>
string(3) "Foo"
string(3) "Bar"

この `new` オペレータコールバックは、必要がなくなったときにunsetできます:

<?php
class Foo {}
class Bar {}

function callback($className) {
    return 'Bar';
}

set_new_overload('callback');
var_dump(get_class(new Foo));

unset_new_overload();
var_dump(get_class(new Foo));
?>
string(3) "Bar"
string(3) "Foo"
クラスのポージング

この `set_new_overload()` 関数は、*クラスのポージング* というプログラム言語の機能を実装することができます。 たとえば、[Objective-C によるクラスポージングの実装](http://en.wikipedia.org/wiki/Objective-C#Posing) では、あるクラスをプログラムで完全に他のクラスに置き換える事を可能にしています。このクラスの置換えは、対象クラスの"ポーズをとる"と呼ばれます。
クラスのポージングには以下の制限があります。

  • クラスは、ただ一つの直系もしく間接的な親クラスのみ、ポーズできる
  • クラスのポージングは、対象クラスに存在しない新しいインスタンス変数を定義してはいけない (ただし、メソッドの定義やオーバーライドはしてもよい)
  • 対象クラスは、ポージングよりも前にあらゆるメッセージを受け取っていてはいけないかも知れない

これらの制限は `ext/test_helpers` によって強制されていません。この拡張モジュールは、(依存性の注入を使うリファクタリングができないようなレガシーなソフトウェアシステム向けの) ユニットテストの開発を楽にすることだけを目的としているからです。

関数の改名

この `rename_function()` 関数は、関数を改名できます:

<?php
function foo()
{
    // ...
}

function foo_stub()
{
    return 'stubbed result';
}

rename_function('foo', 'foo_orig');
rename_function('foo_stub', 'foo');
var_dump(foo());
rename_function('foo', 'foo_stub');
rename_function('foo_orig', 'foo');
?>
string(14) "stubbed result"

これにより、関数のスタブ化/モック化が可能になります。

注意

この拡張モジュールを、Xdebugや `ZEND_NEW` オペコードをオーバーロードするような他の拡張モジュールと併せて使用する場合、競合する拡張モジュールをロードしたあとに `zend_extension` としてロードする必要があります。これは、`php.ini` では次のようになるでしょう:

zend_extension=xdebug.so
zend_extension=test-helpers.so

競合の検出とワークアラウンドが有効かどうかを、`phpinfo()` を使って確認してください。