getter/setterのないprivateプロパティ
外部から提供されているSDKの一部として、あるエラーログクラスがあります。仮に FooLog とします。クラス内部では、PHPの error_log関数を呼んでいます。
このクラスには privateプロパティとして、エラーレベル、出力先があります。このエラーレベルと出力先が期待通りに設定されているか確認する方法がありませんでした。
FooLogを大雑把にコードで示すと次のようなものです。
class FooLog
{
private $level = 'INFO'; // エラーレベル INFO|DEBUG|WARN|ERROR
private $type = 3; // 出力先 0|3 (0=PHPデフォルト、3=ファイル)
public function __construct()
{
// iniファイルがあれば、読み込む
}
public fucntion info($message)
{
...
}
public fucntion error($message)
{
...
}
}
Code language: plaintext (plaintext)
levelプロパティの初期値は "INFO"です。"INFO" の場合、infoメソッドのメッセージも、errorメソッドのメッセージもエラーログに記録されます。
levelプロパティが "ERROR" の場合、infoメソッドのメッセージはエラーログに記録されず、errorメソッドのメッセージだけがエラーログに記録されます。
typeプロパティは、PHPのerror_log の第2引数 message_type です。初期値は 3です。3は ファイル、0 は PHPのデフォルトです。
開発環境では、levelプロパティ="INFO"、typeプロパティ=0(PHPデフォルト)。
本番環境では、levelプロパティ="ERROR"、typeプロパティ=0(PHPデフォルト)とします。
さて、levelプロパティ、typeプロパティどちらもゲッターもセッターもありません。
どうやって設定するかというと、このSDK専用のiniファイルで設定します。FooLogのコンストラクタで、iniファイルがあれば読み込み、iniファイルがなければ初期値のままです。iniファイルがなくても特にエラーはありません。
正しく iniファイルを読み込んで、期待どおりのlevelプロパティとtypeプロパティを設定できているかどうか調べるにはどうすればいいでしょうか?
リフレクションでprivateプロパティを取得する
あー、リフレクション使っちゃったね
ReflrectionProperty
を使って、privateプロパティの値を取得して、テストします。
privateプロパティの値を取得するヘルパーメソッドを作ります。
use ReflectionException;
use ReflectionProperty;
class FooLogTest extends TestCase
{
...
private function getPrivatePropertyValue($obj, $propertyName)
{
$refProperty = new ReflectionProperty(get_class($obj), $propertyName);
$refProperty->setAccessible(true);
return $refProperty->getValue($obj);
}
}
Code language: plaintext (plaintext)
まず、初期値のテストをします。
public function test_初期値()
{
$log = new FooLog();
$this->assertEquals('INFO', $this->getPrivatePropertyValue($log, 'level'));
$this->assertEquals(3, $this->getPrivatePropertyValue($log, 'type'));
}
Code language: plaintext (plaintext)
iniファイルを読み込んだ場合のテストをします。
public function test_iniファイルを読み込んだ場合()
{
// iniファイルを指定の場所に用意しておく
$log = new FooLog();
$this->assertEquals('ERROR', $this->getPrivatePropertyValue($log, 'level'));
$this->assertEquals(0, $this->getPrivatePropertyValue($log, 'type'));
}
Code language: plaintext (plaintext)
ふるまいをテストする
KentBeckが「値をテストするな、ふるまいをテストしよう」と言っているわよ
FooLogクラスのふるまいとは何でしょうか?
FooLogクラスの内部では、error_log関数を呼んでいます。error_log関数を呼んだかどうかがふるまいと言えそうです。
残念ながら、error_log関数をフックするような仕組みは見つかりませんでした。
そこで、error_log関数の出力先であるエラーログファイルを調べることにしました。
エラーログファイルは、ini_set関数で、いつでも変更することができます。
ini_set('error_log', '/tmp/error_log');
Code language: JavaScript (javascript)
何をテストするのか、もう少し具体的に詰めると、
開発環境のiniファイルを読み込むと
(levelプロパティが "INFO"、typeプロパティが 0)
infoメソッドを呼ぶと、エラーログファイルに記録されること。
errorメソッドを呼ぶと、エラーログファイルに記録されること。
本番環境のiniファイルを読み込むと
(levelプロパティが "ERROR"、typeプロパティが 0)
infoメソッドを呼んでも、エラーログファイルに記録されないこと。
errorメソッドを呼ぶと、エラーログファイルに記録されること。
まず、テストケースのsetUpでini_set('error_log', )をテスト用ファイルに記録するようにします。
(以下、phpunit v5.7です)
private $oldErrorLog;
private $newErrorLog = '/tmp/error_log';
public function setUp()
{
parent::setUp();
$this->oldErrorLog = ini_get('error_log');
ini_set('error_log', $this->newErrorLog);
}
Code language: plaintext (plaintext)
tearDownで元に戻します。
public function tearDown()
{
ini_set('error_log', $this->oldErrorLog);
parent::tearDown();
}
Code language: plaintext (plaintext)
エラーログファイルのサイズを0にする、ヘルパーメソッドを作ります。
private function truncateErrorLog()
{
$fp = fopen($this->newErrorLog, 'w');
fclose($fp);
}
Code language: plaintext (plaintext)
開発環境用のテストを書きます。
public function test_開発環境は全レベルを記録すること()
{
// 開発環境用のiniファイルを指定の場所に用意しておく
$log = new FooLog();
$this->truncateErrorLog();
$log->info('dummy');
$this->assertContains('[INFO]', file_get_contents($this->newErrorLog));
$this->truncateErrorLog();
$log->error('dummy');
$this->assertContains('[ERROR]', file_get_contents($this->newErrorLog));
}
Code language: plaintext (plaintext)
次に、本番環境用のテストを書きます。
public function test_本番環境はERRORレベルだけ記録すること()
{
// 本番環境用のiniファイルを指定の場所に用意しておく
$log = new FooLog();
$this->truncateErrorLog();
$log->info('dummy');
$this->assertEquals('', file_get_contents($this->newErrorLog));
$this->truncateErrorLog();
$log->error('dummy');
$this->assertContains('[ERROR]', file_get_contents($this->newErrorLog));
}
Code language: plaintext (plaintext)
まとめ
リフレクションを使って、privateプロパティにアクセスする方法、最終的な保存先のファイルを調べる方法でテストすることができました。
標準関数を上書き
もし、namespaceがあれば、namespaceを使った標準関数を上書きする方法を使うことができます。
FooLogクラスは、外部から提供されているSDKの一部です。FooLogクラスはnamespaceがありませんでした。独自にnamespaceを設定することも考えましたが、開発チームとしては、変更を加えないことにしました。
モンキーパッチ
テストが難しいときは、その場でソースを改変するモンキーパッチという方法があります。今回の例でいうと、FooLogクラス内のerror_log関数の呼び出しを my_error_log関数の呼び出しに置き換えます。