もう過去の遺物と思って避けて通りたいと常々思っているPHP旧バージョンのシステム開発。
残念ながら、古来より存在し、未だに猛威を振るうCMSを利用した開発案件が数多く存在するPHPではレガシーなコードと向き合うことは避けられない。
PHPのレガシーコードでは異常ケースをtrigger_error
を利用してハンドリングするのが定石のようですが、
このケースがモダン?なユニットテストを行う時に問題を起こすのです。
ケース1
以下のようなコードがありました。
class Hoge {
function action($arg) {
if (!is_int($arg)) {
trigger_error('error message', E_USER_ERROR);
return;
}
// some proc...
}
}
このロジックで$arg
が整数でないケースのテストを行いたい といったケースを想定します。
PHPUnitでtrigger_errorを使うとどんな問題が???
上記のコードをテストするために以下のようなテストコードを書きました。
class Hoge_Action_Test extends PHPUnit_Framework_TestCase {
function testArgmentIsNotInteger() {
$hoge = new Hoge();
$this->assertNull($hoge->action('hoge'));
}
}
このテストは失敗します。
PHPUnitは発生するエラーレベルによって例外をスローするため、上記のケースでは例外が発生します。
発生する例外をテストできるようにする
PHPUnitはデフォルトの設定でPHPのエラーをレベル毎に例外へスローするようになっています。
@expectedException
を利用して発生するべき例外を指定すればOKです。
class Hoge_Action_Test extends PHPUnit_Framework_TestCase {
/**
* @expectedException PHPUnit_Framework_Error
*/
function testArgmentIsNotInteger() {
$hoge = new Hoge();
$hoge->action('hoge');
}
}
各エラーレベルと対応する例外は以下の通りです。
E_NOTICE | E_USER_NOTICE | E_STRICT
PHPUnit_Framework_Error_Notice
E_WARNING | E_USER_WARNING
PHPUnit_Framework_Error_Warning
E_DEPRECATED | E_USER_DEPRECATED
PHPUnit_Framework_Error_Deprecated
それ以外
PHPUnit_Framework_Error
ケース2
ケース1のコードにset_error_hundler()
が追加されています
function shutdown($no, $str, $file, $line, $context) {
die();
}
class Hoge {
function __construct() {
set_error_hundler('shutdown');
}
function action($arg) {
if (!is_int($arg)) {
trigger_error('error message', E_USER_ERROR);
return;
}
// some proc...
}
}
この場合ケース1で行ったようなテストは失敗します...
PHPUnitがエラーを例外に置き換えるのはset_error_hundler()
を利用して置き換えているのですが、
処理の実行順序が
1.PHPUnitがエラーを例外に置換する処理を set_error_hundler()
でセット
2.テストコードがエラーのケースを set_error_hundler()
を利用してセット
となり、PHPUnitがせっかく例外に置換する処理を設定してくれているのに、それを上書きするため例外がスローされなくなってしまいます。
どうしましょう?更にset_error_hundlerを上書きしましょう!
テストコード上で更にset_error_hundler()
を使いエラー時の処理を上書きしてしまえばテスト出来ます。
class Hoge_Action_Test extends PHPUnit_Framework_TestCase {
function testArgmentIsNotInteger() {
$hoge = new Hoge();
set_error_hundler(function($no, $str, $file, $line, $context) {});
$this->assertNull($hoge->action('hoge'));
}
}
インスタンスを作らずにテストする方法とかもあるらしいですが、それは検索すればわりと引っかかるので割愛…
そっちのほうがテストコード的に綺麗な気はしますけどね。
ケース3
うわっ、テスト対象メソッドの中にset_error_hundler()
が出てきた!!
function shutdown($no, $str, $file, $line, $context) {
die();
}
class Hoge {
function action($arg) {
set_error_hundler('shutdown');
if (!is_int($arg)) {
trigger_error('error message', E_USER_ERROR);
return;
}
// some proc...
}
}
これはコードを修正しましょう
本当にこの処理は必要なのか?
trigger_error()
をException
にするとか、set_error_hundler()
をコンストラクタ、初期処理に移動するとかを検討。
どうしても必要ならテスト時にrunkit
とか使うしかないかな...
まとめ
CMS開発じゃなくてフルスクラッチで開発したい(泣)