PHP でテストコードを意識したコーディング


どれもこれも常識的な話で恐縮ですが、レガシーシステムと格闘する中で感じることのメモです

1. exit をベタベタ書かない

テスト対象にexitがあると、当然 PHPUnit もそこで終了してしまいます
まずはexitの代わりにreturnで代用できないか考えます

ただ、どうしてもexitが必要な場合があります
例えば header() した直後のexitなどの場合です

その場合はフラグを持たせてやり、phpunit 実行時にはフラグを true にしてやることで exit しないようにしています

こんなイメージです

class Example
{
    // テストフラグ
    private $isTest = false;

    public function sample()
    {
        header("Location: http://www.example.com/");
        // テストじゃなかったら exit させる
        if (!$this->isTest) {
            exit;
        }
    }
}

もちろん、フレームワークを使用している場合はフレームワークが用意してくれているリダイレクトメソッドなりを使用することで、そちらにexitしないフラグも用意されていると思いますので、自前でテストフラグを作る機会は少なくなると思います

2. time() をベタベタ書かない

テスト対象にtime()があると テストが成功したり失敗したりします
テストコードでtime()した期待値と、テスト対象のtime()でズレる可能性があるからです

ありがちなのが、DB に登録日時や更新日時を登録する際に、Model 内でtime()しちゃってそのまま登録している例です

こういう場合はtime()の結果は引数などで外から渡してやるようにするか、time()する箇所を 1 箇所にするかした方がテストしやすいですね

class ExampleModel
{
    public function sample($time)
    {
        // $time を引数で外から渡してもらう
    }

    public function getCurrentTime()
    {
        // もしくは time() 専用の function を作って他では time() しないようにする
        // テスト実行時はこの function を runkit で上書きして固定値にしちゃう
        return time();
    }
}

ただ、既存のほとんどの Model が Model 内でtime()していた場合で、"俺様"が作った Model だけ外からtime()の結果を渡してもらったりすると、"日時"の仕様が変わってきてしまい、なかなか難しいですね

なのでリファクタリングできない場合は、テスト実行時にtime()を runkit で上書き、とかが多い気がします・・

3. require_once をベタべタ書かない

require_onceをあちこちにベタベタ書くと、接着剤をぶちまけたように疎結合でなくなり、スタブやモックを注入し辛くなり、結果テストがし辛くなりますね

なのでできるだけフロントやコントローラーで必要な class を require してやります
もしくはオートローダーを使用し、テスト実行前にスタブやモックをロードしてやれる仕組みを作るのがいいと思います

4. 1 つの function は 1 つの責務で短く

長い function を書いてしまうと、まずテストをするための準備コストが増大しますね
また、色々なテストケースを詰め込んでテストするため、保守もし辛い
その結果、テストコードのテストに時間がかかるようになってしまっては本末転倒だと思います

function が長くなる理由は if 文で全て何とかしようと思うことに原因があると思うので、ポリモーフィズムや DRY を常に意識しています

また、エラーフラグの変数を用意してエラー処理・・とかも 1 function が長くなりがちなので、基本は例外で書くようにしています

1 function を短くするコツはやはり命名にあるので、日頃からセンスを磨いていきたいなあと思います

参考

「現在時刻」を外部入力とする設計と、その実装のこと
http://techlife.cookpad.com/entry/2016/05/30/183947