BEAR.Sundayから学ぶテストプラクティス

## 前置き

先日BEAR.Sunday作者の [koriym](http://qiita.com/koriym) さん とSymfony勉強会でご一緒して話す機会があり、

* echo を テストするにはどうしたいいのか？ 
* HTTPレスポンスヘッダーに想定通りのものが出力されることを確認（header() 関数をテスト）するにはどうしたらいいのか？

※ ここで言うテストとはユニットテストを書くということです

という話をしました。

自分自身気づきもあったし、興味深かったので書くことにしました。

## 解説

まず、テスト対象である`HttpResponder`というクラスについて見てみましょう。

### プロダクトコード HttpResponderクラス

https://github.com/koriym/BEAR.Sunday/blob/develop-2/src/Provide/Transfer/HttpResponder.php

```php
<?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クラス

テストコードはこちらです。

https://github.com/koriym/BEAR.Sunday/blob/develop-2/tests/Provide/Transfer/HttpResponderTest.php

```php
<?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`クラスを継承しています。

テスト対象のクラスを継承したテスト用のクラスを用意し、それに対してテストを実行することで間接的にテストを行います。

これは直接テストしにくいクラスのテストを書く場合に、よく用いられるテストテクニックの１つです。

#### FakeHttpResponderクラス

それでは実際にコードを見てみましょう。

https://github.com/koriym/BEAR.Sunday/blob/develop-2/tests/Provide/Transfer/FakeHttpResponder.php

```php
<?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`の２つのスタティックフィールドを定義しています。

オブジェクトの内部状態にpublicに公開しアクセスすることができるようにし、header関数や echo が呼び出された時に、このスタティックフィールドに保持している値を検証することでテストできるようになります。

#### echoのテスト

それではechoのテストコードを実装する解説に入ります。

先ほどの`FakeHttpResponder`のコードをもう一度見てみます。

```php
    // 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`フィールドに保持するようにしています。

そうすれば、あとはテストコードで確認するだけです。

```php
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でネイティブ関数を含むコードのテスタビリティを上げる２つの方法
http://yudoufu.hatenablog.jp/entry/20110808/1312828535


それでは実際のコードを見ていきましょう。

これまで読み飛ばしてきましたが、何やら`FakeHttpResponder.php`ファイルの上部で`header.php`というファイルを読み込んでいました。

中身は以下のとおり

https://github.com/koriym/BEAR.Sunday/blob/develop-2/tests/Provide/Transfer/header.php

```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()`関数が呼び出された時の引数を保持するようにしています。

テストコードを再度みてみます。

```php
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](http://qiita.com/koriym) さんからお聞きした話ですが、「よくテストコードの名前空間に『Tests』と付けていることが多いけどプロダクトコードは同一の名前空間がよい」とのことです。

理由は２つ

* 別の名前空間で書いてしまった場合、プロダクトコードのテストを書くためにわざわざuse文を使わないいけない
* 今回のようにphp標準関数を置き換えたい場合など、別の名前空間だと面倒だということ

なるほど！と思わされる内容でした。


テストのテクニックを学ぶにはテストコードを読むのが一番です。
有名なライブラリーや、フレームワークのテストコードはどんどん読むようにしてみてください。