Do You PHP はてブロ

Do You PHPはてなからはてブロに移動しました

TestCaseのコンストラクタは、含まれるtestメソッド数分だけ呼び出される

知ってる人は知ってると思いますが、ちょっと「えー!」な感じだったので。
TestCaseオブジェクトのsetUpメソッドやtearDownメソッドは、testメソッドが実行される前後にそれぞれ呼び出されます。つまり、それぞれ、testメソッド数分呼び出されます。これはよくドキュメントに書かれているので知っていると思います。
一方、コンストラクタはどうかというと。。。

背景

最近、テストの数(ファイル数、testメソッド数、assert数)が増えてきて、全テスト実行するのに30分以上かかっています。中には、DBに接続してSQLを実行して想定している結果が返ってくることをテストしていたり、HTTPリクエストを送信してステータスコードやContent-Typeヘッダ、レスポンスボディの中身が一致しているかどうかのテストなども行っているので、それなりに時間がかかるといえばかかるようになっています。ただ、SQLを直に実行した場合のパフォーマンスと比べて、思ったよりも時間がかかっている気がしてました。

調査

PHPUnit 3.4.15で調べてみると、TestCaseオブジェクトのコンストラクタが呼び出されるまでのスタックトレースは次のような感じで、一度TestSuiteオブジェクトを作成して、そこにテストを追加していく、といった流れになります。

SampleTest.__construct(TestSuite.php:573)
  at PHPUnit_Framework_TestSuite.createTest(TestSuite.php:829)
  at PHPUnit_Framework_TestSuite.addTestMethod(TestSuite.php:233)
  at PHPUnit_Framework_TestSuite.__construct(BaseTestRunner.php:147)
  at PHPUnit_Runner_BaseTestRunner.getTest(Command.php:167)
  at PHPUnit_TextUI_Command.run(Command.php:146)
  at PHPUnit_TextUI_Command.main(phpunit:54)

で、PHPUnit_Framework_TestSuiteの__constructメソッド内の233行目付近に以下のコードがありました。変数$theClassはReflectionClassオブジェクトです。

<?php
class PHPUnit_Framework_TestSuite implements PHPUnit_Framework_Test, PHPUnit_Framework_SelfDescribing, IteratorAggregate
{
                            :
    public function __construct($theClass = '', $name = '')
    {
                            :
        foreach ($theClass->getMethods() as $method) {
            if (strpos($method->getDeclaringClass()->getName(), 'PHPUnit_') !== 0) {
                $this->addTestMethod($theClass, $method, $names);
            }
        }
                            :

ここでメソッド数分だけループしているところがポイント。
addTestMethodメソッドは、内部で静的メソッドのcreateTestメソッドを呼び出します。このcreateTestメソッドでTestCaseクラスをインスタンス化しています。
つまり、

含まれるtestメソッドの個数分だけ、TestCaseクラスのコンストラクタが呼び出される

ということになります。

ということは

TestCaseの書き方にも気を使わなきゃいけない、ということですね。たとえば、テストの実行速度が思わしくない場合、次のようなところに気をつけたら良さそうです。

  • TestCaseのコンストラクタ、setUp/tearDownメソッドに重い処理を書いてないかどうかをチェック
    • 『共通処理だから』といって、安易に上記メソッドに書かないようにする
    • とは言え、あちこちに同じようなことを書いちゃうと、テストのメンテナンスコスト上がるし。。。
  • テスト対象の処理自体が重い場合、その処理をチューニングする(SQLやループでの処理など)

というのを考えてたら

第2回設計勉強会で『みなさん、どの粒度でテスト書いてます?』という質問をしたんですが、その辺にもなんか当てはめられそうな感じもしてきた。。。
まあ、実行速度だけを重視してもダメなんですが、実行速度は結構重要と思ってます。いつまで経っても終わらないテストはやりたいと思えませんし。うまく落しどころを探りたいなぁ。

追記(2010/08/13 10:52)

PHPUnitのマニュアルにありますが、setUp/tearDownやtestメソッド以外に、色々なタイミングで呼び出されるメソッドが用意されています。これをうまく使うのも1つの手ですね。