前置き
先日BEAR.Sunday作者の koriym さん とSymfony勉強会でご一緒して話す機会があり、
- echo を テストするにはどうしたいいのか?
- HTTPレスポンスヘッダーに想定通りのものが出力されることを確認(header() 関数をテスト)するにはどうしたらいいのか?
※ ここで言うテストとはユニットテストを書くということです
という話をしました。
自分自身気づきもあったし、興味深かったので書くことにしました。
解説
まず、テスト対象であるHttpResponder
というクラスについて見てみましょう。
プロダクトコード HttpResponderクラス
<?php
/**
* This file is part of the *** package
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace BEAR\Sunday\Provide\Transfer;
use BEAR\Resource\ResourceObject;
use BEAR\Sunday\Extension\Transfer\TransferInterface;
class HttpResponder implements TransferInterface
{
public function __invoke(ResourceObject $resourceObject)
{
// code
http_response_code($resourceObject->code);
// header
foreach ($resourceObject->headers as $label => $value) {
header("{$label}: {$value}", false);
}
// body
echo (string) $resourceObject;
}
}
このクラスは名前の通りHTTPレスポンスの「HTTPステータスコード」「ヘッダー」「ボディー」の出力に責任を持つクラスとなっています。実装はシンプルですね。
ただしここで問題。
- http_response_code()や、header()関数が呼ばれること
- echo で文字列が出力されたこと
は果たしてどのようにしてテストすればよいのでしょうか?
みなさんこのテストコードが書けるでしょうか?
もし書くことができれば、普段からテストコードをバリバリ書いているTDDの猛者に違いありません(笑)
それでは順番に解説していきます。
テストコード HttpResponderTestクラス
テストコードはこちらです。
<?php
namespace BEAR\Sunday\Provide\Transfer;
use BEAR\Sunday\Fake\Resource\FakeResource;
class HttpResponderTest extends \PHPUnit_Framework_TestCase
{
/**
* @var HttpResponder
*/
private $responder;
public function setUp()
{
parent::setUp();
$this->responder = new FakeHttpResponder;
FakeHttpResponder::reset();
}
public function testTransfer()
{
$ro = (new FakeResource)->onGet();
$ro->transfer($this->responder);
$expectedArgs = [['Cache-Control: max-age=0', false]];
$this->assertEquals($expectedArgs, FakeHttpResponder::$headers);
$expect = '{"greeting":"hello world"}';
$actual = FakeHttpResponder::$content;
$this->assertSame($expect, $actual);
}
}
FakeHttpResponder
というクラスを使うことで問題を解決していることが分かります。
勘の鋭い方はこの時点で気付かれているかもしれませんね。
実はFakeHttpResponder
というクラスは、本来のテスト対象であるHttpResponder
クラスを継承しています。
テスト対象のクラスを継承したテスト用のクラスを用意し、それに対してテストを実行することで間接的にテストを行います。
これは直接テストしにくいクラスのテストを書く場合に、よく用いられるテストテクニックの1つです。
FakeHttpResponderクラス
それでは実際にコードを見てみましょう。
<?php
namespace BEAR\Sunday\Provide\Transfer;
use BEAR\Resource\ResourceObject;
require_once __DIR__ . '/header.php';
class FakeHttpResponder extends HttpResponder
{
// スタティックフィールドを追加しています
public static $headers = [];
public static $content;
public static function reset()
{
static::$headers = [];
static::$content = null;
}
// 元のメソッドをオーバーライドしてます
public function __invoke(ResourceObject $resourceObject)
{
ob_start();
parent::__invoke($resourceObject);
$body = ob_get_clean();
self::$content = $body;
}
}
この継承後のFakeHttpResponder
クラスでは、publicな$headers
と$content
の2つのスタティックフィールドを定義しています。
オブジェクトの内部状態にpublicに公開しアクセスすることができるようにし、header関数や echo が呼び出された時に、このスタティックフィールドに保持している値を検証することでテストできるようになります。
echoのテスト
それではechoのテストコードを実装する解説に入ります。
先ほどのFakeHttpResponder
のコードをもう一度見てみます。
// FakeHttpResponder クラスのコードから抜粋
public function __invoke(ResourceObject $resourceObject)
{
ob_start();
parent::__invoke($resourceObject);
$body = ob_get_clean();
self::$content = $body;
}
HttpResponder
クラスの__invoke()
メソッドをオーバーライドし、親のメソッドを呼び出す際に、ob_start()
とob_get_clean
で囲っています。
ob_start()
を使うことで本来ならecho文で出力される出力内容をバッファリングし、ob_get_clean()
でその値を取り出します。そしてその値を$content
フィールドに保持するようにしています。
そうすれば、あとはテストコードで確認するだけです。
class HttpResponderTest extends \PHPUnit_Framework_TestCase
{
public function testTransfer()
{
// ここより上は省略
$expect = '{"greeting":"hello world"}';
$actual = FakeHttpResponder::$content;
$this->assertSame($expect, $actual);
}
}
簡単ですね。
headerのテスト
さて、headerのテストです。
headerのテストですが、ここではphp標準関数であるheader関数を呼び出していてPHPUnitではどう頑張ってもHTTPヘッダーが想定通りに返っているのかテストできそうにありません。
この問題はどう解決すればよいのか?
ここでは更に別のテクニックを利用することで問題を解決します。
これはPHPならではですが「クラスと同一の名前空間に関数を定義するとそちらの関数が優先で使われる」というPHPの言語仕様を利用してテストを行います。
この仕様を利用することでPHPの標準関数をオーバーライドすることができ、自由にテストを行うことができます。
【参考】
PHP版レガシーコード改善に役立つ新パターン
http://www.slideshare.net/techblogyahoo/php-wewlcjp
PHPでネイティブ関数を含むコードのテスタビリティを上げる2つの方法
http://yudoufu.hatenablog.jp/entry/20110808/1312828535
それでは実際のコードを見ていきましょう。
これまで読み飛ばしてきましたが、何やらFakeHttpResponder.php
ファイルの上部でheader.php
というファイルを読み込んでいました。
中身は以下のとおり
<?php
namespace BEAR\Sunday\Provide\Transfer;
function header($string, $replace = true, $http_response_code = null)
{
FakeHttpResponder::$headers[] = func_get_args();
}
ここでは、BEAR\Sunday\Provide\Transfer
という名前空間に、header関数を定義しPHP標準関数のであるheader関数を置き換えています。
このheader関数が呼び出された際にFakeHttpResponder
の$headers[]
フィールドにheader()
関数が呼び出された時の引数を保持するようにしています。
テストコードを再度みてみます。
class HttpResponderTest extends \PHPUnit_Framework_TestCase
{
public function testTransfer()
{
$ro = (new FakeResource)->onGet();
$ro->transfer($this->responder);
$expectedArgs = [['Cache-Control: max-age=0', false]];
$this->assertEquals($expectedArgs, FakeHttpResponder::$headers);
}
}
FakeResource
の実装を追ってもらうと分かりますが、$expectedArgs
で書いている通り確かにheader('Cache-Control: max-age=0', false)
で呼び出しを行っています。
あとは、assertEquals
で確認するだけです。
まとめ
- テストしにくいクラスはテスト用のクラスを用意し、テスト可能になるようにメソッドをオーバーライドしたり、フィールドなどを定義したりしましょう。
- PHPの標準関数を置き換えないとどうしてもテストできない場合には同一の名前空間に同一のメソッドを定義しましょう
- (稀なケースだとは思いますが) echo文をテストしたい場合には
ob_start()
を利用しましょう
以上、BEAR.Sundayのテストコードから実践的なテスト手法を学びました。
最後に非常に参考になった話
これは koriym さんからお聞きした話ですが、「よくテストコードの名前空間に『Tests』と付けていることが多いけどプロダクトコードは同一の名前空間がよい」とのことです。
理由は2つ
- 別の名前空間で書いてしまった場合、プロダクトコードのテストを書くためにわざわざuse文を使わないいけない
- 今回のようにphp標準関数を置き換えたい場合など、別の名前空間だと面倒だということ
なるほど!と思わされる内容でした。
テストのテクニックを学ぶにはテストコードを読むのが一番です。
有名なライブラリーや、フレームワークのテストコードはどんどん読むようにしてみてください。