ハウテレビジョン開発者ブログ

『外資就活ドットコム』を日夜開発している技術陣がプログラミングネタ・業務改善ネタ・よしなしごとについて記していきます。

CakePHP2でコントローラーのテストをする時の redirect を無視させない

ここ2週間ほど夏風邪でお粥とうどんしか食べていなかったのに、なぜか体重が2キロ増えた津田(id:YTsuda)です。 フロントエンドを中心に開発全般何でもやっています。

今回はハマりどころの多い CakePHPのControllerのテストについて書きます。

環境

CakePHP2系で、PHPUnitを使ってテストをしているという前提で進めます。

redirect がスルーされる問題

Controller のテストを実行する時に、$this->redirect() がスルーされてしまう、という問題があります。 公式ドキュメントにも以下のような記述があります。

<?php
class ArticlesController extends AppController {
    public function add() {
        if ($this->request->is('post')) {
            if ($this->Article->save($this->request->data)) {
                $this->redirect( array('action' => 'index') );
            }
        }
        // more code
    }
}

上記のコードをテストすると、リダイレクトに到達したにもかかわらず // more code が 実行されてしまいます。

引用: コントローラーのテスト :: テスト — CakePHP Cookbook 2.x ドキュメント

そして、同ドキュメントでは // more code が実行されないようにする方法として、 以下のように、return を推奨しています。

<?php
class ArticlesController extends AppController {
    public function add() {
        if ($this->request->is('post')) {
            if ($this->Article->save($this->request->data)) {
                // ここで return する
                return $this->redirect( array('action' => 'index') );
            }
        }
        // more code
    }
}

しかし、この方法では解決できないケースもあります。 例えば、別に切り分けたメソッドの中で redirect をしているケースです。

<?php
class ExamplesController extends AppController {

    public function add() {
        $this->_redirectMethod();  // メソッド内でリダイレクト
        // more code
    }
    
    // リダイレクトさせるメソッド(適当)
    private function _redirectMethod(){
        return $this->redirect('/'); // return しても add() に戻るだけ
    }
}

この場合 _redirectMethod で return しても、addメソッドに戻るだけなので // more code は実行されてしまいます。 このケースでは元コードを構えば楽に対応できますが、Component で redirect を使う場合も同様の問題をはらんでいます。 そのため、今回は根本的な解決方法を探ってみました。

解決法

今回は、redirect をさらにモックして Exception 投げるようにしてみました。

<?php
class ExamplesControllerTest extends ControllerTestCase {

    public function testAddRedirect(){

        $this->Controller = $this->generate('Examples', array(
            'methods' => array('redirect')
        ));

        $this->Controller->expects($this->any())
            ->method('redirect')
            ->will(
                $this->throwException(new ForbiddenException('error')));

        $this->setExpectedException('ForbiddenException');

        $this->testAction('/examples/add' );
    }
}

これで期待どおり ForbiddenException が発生し、テストが成功しているはずです。 順を追って説明します

1. Controllerのモックを作る

まず Controller 全体をモックしています 。

<?php
$this->Controller = $this->generate('Examples', array(
    'methods' => array('redirect')
));

参考: テストアクションによるモックの使用 :: テスト — CakePHP Cookbook 2.x ドキュメント

2. モックの挙動を設定

さらに redirect が ForbiddenException を投げるようにモックの挙動を設定します。もちろんForbiddenでなくても結構です。

<?php
$this->Controller->expects($this->any())
          ->method('redirect')
          ->will($this->throwException(new ForbiddenException('error')));

参考: PHPUnit マニュアル – 第10章 テストダブル

3. assertion

そして、ForbiddenException が起こることを期待する assertion です。

<?php
$this->setExpectedException('ForbiddenException');

参考: 例 4.11: テスト対象のコードで発生するであろう例外の指定 :: PHPUnit マニュアル – 第4章 PHPUnit 用のテストの書き方

4. getリクエストのテストを実行

最後に testAction で get リクエストのテストを走らせます。

<?php
$this->testAction('/examples/add' );

参考: GETリクエストのシミュレート :: テスト — CakePHP Cookbook 2.x ドキュメント

おわり!

Controllerのテストを書くのは大変で、ハマりどころも多いですが、(ACLなど)ミスがあったら致命的な問題が発生する箇所でもあります。 めんどくさがらずに書いていきましょう。