2014年9月4日木曜日

PHPUnitを使ってheader()なメソッドのテストを書く方法のメモ

結構な悩みどころよね(´・ω・`)


テストを書きたい。でもどう書けば良いのか分からない。大体PHPUnit使ってて思うことはこれ。

以前書いたDB周りのテストをする方法も結構悩んだところだったんだけど、ボクにとってもうひとつ書き方が分からなかったブツがある。

それは「header()」を使ってるメソッドのテスト。ボクが作ってるPHP向けイケてるフレームワークである「Risoluto」でもそういうメソッドがあるんだけど、しばらくの間どうテストを書けばいいのか分からずにいた。

で、いろいろ調べてみたところ比較的簡単にかけるってことが分かったので、その手順をまとめておこうというのがこのエントリの趣旨。

前提条件


このエントリでは、

  • PHPUnitを使用すること
  • Xdebugが有効になっていること

というのを前提条件にしている。

また、このエントリでも完璧なテストは無理。例えば、「header()」の第3引数にHTTPステータスコードを指定できるけど、これをチェックする方法をまだボクは見つけていない(*1)。

実際のテストケースコードを見てくれたまえ


最初にテストケースの例を。下記はRisolutoで採用したテストケースをベースにちょっと汎用的に見えるよう書き直したものだ。

<?php

/**
 * @runTestsInSeparateProcesses
 */
class UrlTest4RedirectTo extends \PHPUnit_Framework_TestCase
{
    /**
     * setUp()
     *
     * テストに必要な準備を実施
     */
    protected function setUp()
    {
        // 拡張モジュールがロードされているかをチェック
        if (!extension_loaded('xdebug')) {
            $this->markTestSkipped('Cannot use xdebug expansion module.');
        }
    }

    /**
     * testSample()
     *
     * テストの例
     */
    public function testSample()
    {
        header('Location: http://localhost/');
        $output_header = xdebug_get_headers();

        $this->assertContains('Location: http://localhost/', $output_header);
    }
}

このコードをベースにポイントをチェックしていこう。

@runTestsInSeparateProcesses


テストクラスに「@runTestsInSeparateProcesses」というアノテーションを書いておく必要がある。ボクの場合は、「header()」を使ったテストをひとつのクラスにまとめてあるのでクラスの方にこのアノテーションを書いているけど、メソッド単位で指定したければ「@runInSeparateProcess」を使えばOK。

これはテストクラス内のすべてのテストケースを、個別の PHP プロセスで実行するように指示するというものなんだけど、これが無いとテスト中に発生する様々な出力によって(?)「header()」が上手く動いてくれなかったりする(*2)。

最初のハマりポイントだったことは言うまでもない。

拡張モジュールがロードされているかのチェック


これは必須じゃないんだけど、Xdebugが使えない環境でテスト流したときに軒並みエラーになるのを防ぎたいことがある。ので、ボクは「setUp()」でXdebugが使える状態かをチェックして、使えない環境ならテストをスキップするようなコードを書いてみた。

なんでXdebugが必要なのか。それは次の項目で。

xdebug_get_headers()


通常、出力内容を取得したりする時は「ob_start()」でバッファリングしてゴニョゴニョするんだろうけど、これがボクの環境では上手く動かなかった。ついでに「headers_list()」も期待通り動かなかった。

そこで代わりに使えるのが「xdebug_get_headers()」ってメソッド。これが使いたいがためにXdebugが必須ってことにしているのだ。

このメソッドでは「header()」や「setcookie()」などを使っているなら、このメソッドで取得できるので覚えておくと良いかも。

ただ、完全なHTTPリクエストヘッダを生成してくれるわけじゃないので、HTTPステータスコードとかはチェックできない。

assertContains()


で、前述の「xdebug_get_headers()」で取得した結果をチェックするのに便利なのが、「assertContains()」ってメソッド。

イメージ的には「in_array()」みたいなノリで使えるって感じ。

似たような名称を持つメソッドとして、「assertNotContains()」とか「assertContainsOnly()」とか「assertContainsOnlyInstancesOf()」なんてのがある。

他にもいっぱいアサーションメソッドがあるから、「アサーション」ページに目を通しておくといいんじゃないかな。

-----------------------------------------
【2014/09/04 21:46 追記】

bootstrapでの定数定義に起因する問題について


ボクがちょっと嵌まったポイント。

PHPUnitでは「--bootstrap」オプションによりテスト実行前に任意のPHPファイルを実行することができるんだけれども、この中で定数定義をしている場合、「@runTestsInSeparateProcesses」や「@runInSeparateProcess」のアノテーションを使用していると下記のようなエラーが出ることがある。

PHPUnit_Framework_Exception: PHP Notice:  Constant CONSTNAME already defined in /path/to/dir/bootstrap.php on line __LINE__

要は「定数が重複して定義されてますよ!」ってお話。

bootstrapなコード中で

define('CONSTNAME', 'const_value');

のように書いているなら、これを

if(!defined('CONSTNAME')){
    define('CONSTNAME', 'const_value');
}

のようにすると回避できる。

defined()」を使って定義済みかをチェックし、未定義の時だけ「define()」で定義するという流れ。

-----------------------------------------

これで勝てる!


そんなこんなで「header()」を使ったメソッドがかけるようになった。ヤッタネ!あとはセッション周りとファイルアクセスまわりのテストをどう書くかが固まればパーペキなんだけど、それはおいおい調べて and/or 考えていこう。

テストといえば、本件を調べている途中に興味深いエントリを見つけた。「モダンテスティングフレームワーク Codeception」なんてのがあるらしい。Composerでもインストールできるらしいので、PHPUnitから乗り換えてみるのもいいかもなんて思った。


あ、あとこのエントリを書くにあたって下記を参考にしました。



*1:だれか教えてくれないかな(´・ω・`)
*2:「Cannot modify header information - headers already sent by (output started at /path/to/dir/test.php:__LINE__)」的なエラーが出たりする

0 件のコメント:

コメントを投稿