ContollerのTestのはまりどころ(redirect)

最近、テスト廚ぎみなわたなべです(^^;

ビジネスロジックは出来るだけModelやComponentに書く様にしているのでModelやComponenntなどのテストはそれなりに書いていたのですが、Controllerのテストは全く書いていませんでした。とはいえ、Controllerにも処理があるので、テストを書いてみようと試したときにはまった点と私なりの解決方法をまとめてみました。

Controllerをbakeすると自動で作られるControllerのTestCodeは以下のような感じです。

<?php 
/* SVN FILE: $Id$ */
/* ExampleController Test cases generated on: 2009-12-04 19:56:41 : 1259924201*/
App::import('Controller', 'Examples');

class TestExamples extends ExamplesController {
  var $autoRender = false;
}

class ExamplesControllerTest extends CakeTestCase {
  var $Examples = null;

  function startTest() {
    $this->Examples = new TestExamples();
    $this->Examples->constructClasses();
  }

  function testExamplesControllerInstance() {
    $this->assertTrue(is_a($this->Examples, 'ExamplesController'));
  }

  function endTest() {
    unset($this->Examples);
  }
}

で、以下のようなテストコードを書いてみました。

<?php
  // テストクラスの頭に以下のfixture定義を追加
  var $fixtures = array('example');
....
  function testIndex()
  {
    $result = $this->testAction('/admin/examples/', array('fixturize' => true, 'return'=>'vars'));

    $expected = array(
      array(
        'Example' => array(
          'id' => '1',
          'name'  => 'EXAMPLE1',
          'created'  => '2009-12-03 19:40:59',
          'modified'  => '2009-12-03 19:40:59'
        )
      ),
    );
    $this->assertEqual($results['examples'], $expected) ;
  }

textActionのPATHを見ていただければ分かる様に、このControllerは管理画面用のControllerで認証が通ってなかったり、権限がないとredirectしまくります。
テストを実行してみたところ、通常の遷移と同様にログイン画面に遷移してしまいました。

bakeされたコードにTest用のControllerのひな形があったので、このコントローラでredirectメソッドを書き換えちゃえばオッケー!と、思って試したところ撃沈...。

CakeTestCaseクラスの中を追いかけてみたのですが、Test用のControllerで置き換える様な仕掛けは見つけられませんでした。googleさんで調べてみた所、同じ様なところで皆さん苦労されているようで以下の方法にたどり着きました。

runkitは普段の開発環境のMacOSX上で動作させられなかったので、断念。
test.phpを書き換える方法は、Controllerにテスト用のコードが混ざってしまうのが気になるので避けたい...。

という事でTest用のDispatcherを弄ったりしてみたのですが、どうもしっくりきません。諦めかけていた所でひらめきました(^^!

「routingテーブルを書き換えちゃえば良いじゃん!」

あっさり動きました!以下がテストコードです。

<?php 
/* SVN FILE: $Id$ */
/* ExampleController Test cases generated on: 2009-12-04 19:56:41 : 1259924201*/
App::import('Controller', 'Examples');

// Test用のコントローラ名をbakeされた物(TestExamples)からTestExamplesControllerに変更
class TestExamplesController extends ExamplesController {
  var $autoRender = true;

    // redirect処理を上書き
  function redirect($url=NULL,$code=NULL)
  { 
    $this->lastRedirectUrl = $url;    
  }
}

class ExamplesControllerTest extends CakeTestCase {
  var $Examples = null;
  var $fixtures = array('example');

  function __construct()
  {
    parent::__construct() ;
      // ルーティングテーブルにテスト用コントローラを追加
    $Router = Router::getInstance() ;
    Router::connect('/admin/examples/:action/*', array('controller' => 'test_examples', 'admin' => true));    
  }

  function startTest() {
    $this->Examples = new TestExamplesController();
    $this->Examples->constructClasses();
  }

  function testExamplesControllerInstance() {
    $this->assertTrue(is_a($this->Examples, 'TestExamplesController'));
  }

  function endTest() {
    unset($this->Examples);
  }

  function testIndex()
  {
    $result = $this->testAction('/admin/examples/', array('fixturize' => true, 'return'=>'vars'));

    $expected = array(
      array(
        'Example' => array(
          'id' => '1',
          'name'  => 'EXAMPLE1',
          'created'  => '2009-12-03 19:40:59',
          'modified'  => '2009-12-03 19:40:59'
        )
      ),
    );
    $this->assertEqual($results['examples'], $expected) ;
  }
}

あとは、bakeのテンプレートを書き換えてしまえばいい感じになりそうです。

みなさん、Controllerのテストはどうしてますか?
「もっと簡単な方法があるよ!」とか「標準の機能でできるよ!」とかあれば是非教えてほしいです!

12/7 追記

CookbookのコントローラのTestのマニュアルページに対策が書いてありますね(^^;

CakeTestDispatcher::__loadControllerを書き換える方法なのです。
同じ様な方法で動作する事も確認していたのですが、,Coreのコードを触るのは出来るだけ避けたかったので、やめた方法でした(^^;

このページ見ていたはずですが、redirectを書き換える所だけ見て下の方は見てませんでした(;_;。

12/8 追記

$this->params['requested'] の有無で分岐する方法もとれそうですね。
いずれにせよテストしやすい様にする為に、Controller内でテストからの実行時に処理を分けるのは必要そうな感じ。

12/8 追記^2

結局、リダイレクト先のチェックをするのに実行されたControllerのインスタンスが必要だったので以下の様にCakeTestCaseを修正してみました。

<?php
  function startController(&$controller, $params = array()) {
    $this->___controller =& $controller ;
....  
  function &getController()
  {
    return $this->___controller ;  
  }