2013/08/05

CakePHPでもテスト ~手を抜くためにする遠回り~

CakePHPでもテスト!テスト!テスト!

テストドリブンと行きたいところですが、既にコード書いちゃった~。なんて興が乗っちゃって気づいたらかなり書き進んでしまったという事はプログラマあるあるだと思いますが、今から一からテストケース書くのって途方に暮れる事になりますよね。
CakePHP2系はPHPUnitを利用しているようなので、phpunit-skelgenが使えそうですが、extends周りのしがらみで、思ったように動いてくれません。一からコードを書くことなんてありえないCakePHPでは、AppControllerクラスを継承していて当たり前、AppModelクラスを継承していて当たり前。
そもそも、CakePHPのフレームワークにはPHPunitが組み込まれていて、ちゃんと上の問題を解消した独自のテストケースクラスが用意されていますので、それを使います。
じゃあphpunit-skelgenが使えない。でもね。CakePHPには、bakeがあるじゃない。




bakeでテストケースのスケルトンを作る

コンソールからbakeを使えば、phpunit-skelgenと同等のものを手軽に生成できます。もちろん、今まで書いた関数をカバーしたスケルトン。テストファーストではないけれど、テストの重要性については理解している人たちの強い味方です。

使い方。
cd /path/to/cake/app/
./cake bake test
# sudoしないとテンポラリが書き込めねぇよと怒られることもしばしば。動くは動きますので無視可。

Welcome to CakePHP v2.3.6 Console
---------------------------------------------------------------
App : app
Path: /path/to/cake/app/
---------------------------------------------------------------
---------------------------------------------------------------
Bake Tests
Path: /path/to/cake/app/Test/
---------------------------------------------------------------
---------------------------------------------------------------
Select an object type:
---------------------------------------------------------------
1. Model
2. Controller
3. Component
4. Behavior
5. Helper
Enter the type of object to bake a test for or (q)uit (1/2/3/4/5/q) 

[q] > 

とこういう形になります。
1→エンターキーでモデルの一覧が表示されます。テストケースのスケルトンを作りたいモデルの番号を選べば、完了です。

調子に乗ってどんどん作っていった訳ですが、コントローラーのテストを作る時にどうしてもエラーが出るものがありました。
Fatal Error Error: Class 'AppController' not found in [/path/to/cake/app/Controller/UsersController.php, line 4]

AppControllerがないと言われても、普通に動いとるがなと思いながら、何か根本的な部分を見落としている気がして、ふと思い立ってbakeでコントローラーを新規に作ってみたところ、先頭にこれがなかった。
App::uses('AppController', 'Controller');

なるほど~。ちゃんと細かいところも見てるんだねぇ~、と思った次第です。

続いてFixtureの作成

Fixtureは、テストで使うDB用のデータを置いたりするファイルのことですが、ガリガリ書いてある程度動いちゃってる場合は、DBにテスト用のデータが入ってる事もありますよね? ありますよ。
そういう場合でも、既存のテーブルからFixtureをこしらえてくれるのが、bakeです。

cd /path/to/cake/app/
./cake bake fixture

Welcome to CakePHP v2.3.6 Console
---------------------------------------------------------------
App : app
Path: /path/to/cake/app/
---------------------------------------------------------------
---------------------------------------------------------------
Bake Fixture
Path: /path/to/cake/app/Test/Fixture/
---------------------------------------------------------------
Possible Models based on your current database:
1. User
Enter a number from the list above,
type in the name of another model, or 'q' to exit  
[q] > 1
Would you like to import schema for this fixture? (y/n) 
[n] > n
Would you like to use record importing for this fixture? (y/n) 
[n] > n
Would you like to build this fixture with data from User's table? (y/n) 
[n] > y
Please provide a SQL fragment to use as conditions
Example: WHERE 1=1  
[WHERE 1=1] > where 1=1
How many records do you want to import?  
[10] > 10

Baking test fixture for User...

File `/path/to/cake/app/Test/Fixture/UserFixture.php` exists
Do you want to overwrite? (y/n/q) 
[n] > y
Wrote `/path/to/cake/app/Test/Fixture/UserFixture.php`

キモになるのは、
Would you like to import schema for this fixture? (y/n)
Would you like to use record importing for this fixture? (y/n)
の2つの質問の部分で、何かというと、上の例では、Userモデル用のテーブルをインポートしますか?と聞かれ、「n(No)」を返し、このテーブルのレコードをインポートしますか?と聞かれ「n(No)」を返しています。
「y(Yes)」を返すと、テストの度に本番のテーブルからレコードを引っ張ってくる事になりますが、テストで使うデータが都度違うのもどうかなという事で、本番のテーブルをインポートしないようにしました。
仕様の変更に伴い、テーブル構造が変わったりする場合は、派手にエラーが出るのでその時はFixtureを作り直しましょう。:-P

Would you like to import schema for this fixture?で「n」を選んだ場合はテーブル構造がFixtureに書きだされます。
Would you like to use record importing for this fixture?で「n」を選んだ場合はレコードがFixtureに書きだされます。
レコードは全部ではなく、WHERE句とLIMIT句が指定できるので、テスト効率のいいレコードや、複数回通す意味が無い冗長なレコードを弾くこともできます。まぁ、どちらもファイルに書きだされているので、後で編集することもできます。


2013/08/06 追記
Fixtureにレコードを書きだしてテストで使う場合、Config/database.phpの$testにテスト用のデータソースを設定する必要があります。

ここは設定していたので問題ないのですが、テストをしているとFixtureのデータが反映されていない事に気付いて色々調べた結果、Modelの$useDbConfigを'test'に書き換えないとFixtureを使って突っ込んだデータでテストを行わない事が発覚。
テスト終わったら、この部分をコメントアウトするのか? なんでテストコードが混入するんだと思って、Testの方から$this->User->useDbConfig = 'test'とかしても反映されないので諦めてしまった(これもおかしい・・・)。

修正→テスト→本番の度にAppModelクラスのvar $useDbConfig = 'test';をコメントアウトする必要があるとか、かなりめんどうなので何かいい方法があるはずなんだろうが、ここまでしか気力が続かなかった・・・。

何かいい方法やおかしい事をしていたらコメントからこっそり教えて下さい!:-)

いざテスト!

これだけ揃えば、とりあえずテストする事は可能ですので、一度回してみます。ただし、テストケースがないので、オールクリアになりますが。

テストはウェブから結果を確認できます。URLはhttp://localhost/[appディレクトリ]/test.phpから確認できます。

ここまでは自動でできますが、個々のテストは自分で書く必要があります。

テストケースの中で使えるアサーションはPHPUnitに準じている(というかPHPUnitをラップしているので当然です)ので、PHPUnitのドキュメントを参照しながら、効率のいいテストを書いていきます。


ちなみに、XDebugがインストールされているとコードカバレッジが表示されます。テストが通っていない分岐などがグラフィカルに分かるので、これもたまに眺めるといいことがあるかも知れません。

XDebugはgithubから最新版が手に入ります。
インストール方法はreadmeに書いてあります。英語ですが簡単に読めますし、細かく書いてくれているのでトライしてみてください。


2 件のコメント:

  1. こんにちは。
    $useDbConfigの追記についてですが、解決されたでしょうか?
    僕も同じ問題に当たりました。

    テスト対象のコードで、

    App::import('Model', 'HogeModel');
    $HogeModel = new HogeModel();

    としていないでしょうか?
    newでモデルをインスタンス化すると、Config/database.phpの「default」が使われるようです(「test」を設定していたとしても)。
    代わりに以下でモデルインスタンスを得ると、「test」が使われました。

    ClassRegistry::init('HogeModel');

    巷のサイトでは、CakePHP 2.xだと、「モデルのロードにはApp::import()を使う」といったことが書いてあるところが多いので、上記のようにClassRegistry::init()を使うのが正しいのかはわかりません。

    返信削除
    返信
    1. >paranoidさん
      情報ありがとうございます!
      結局何も進展がないまま来ていましたが、いただいた情報で調べてみると、Cookbookにありましたねぇ。

      http://book.cakephp.org/2.0/ja/development/testing.html

      手元に確認できるコードがないのが悔やまれるところですが、Cookbookへの誘導は本文にマージしておこうと思います。
      最近速度面の都合でマイクロフレームワークを使ったりすることが多いですが、全部入りのCakePHPの至れり尽くせり感が心地よくも感じますね。

      削除