ホーム > PHP > CakePHP

CakePHP アーカイブ

CakePHPプラグインのまとめ – 検索とページネーション/ファイルアップロード編

cakephp_plugins.png

前回の”CakePHPプラグインのまとめ – 認証編“の続きです。
以下のページの”Searching and Pagination”と”File Uploading”の訳とメモを書いていきます。

検索とページネーション:

  • CakeDC’s Search: 少しばかり高度だが、ページネーションにフィルタリングを掛ける確実な方法だ。CakePHPのエキスパート自身によって書かれているよ。恐らくもっとテストが必要だけどね ;)
  • 面倒な検索とページネーションの処理を一括して面倒を見てくれるプラグインのようです。READMEには複合検索のスニペットが載っていますが、このプラグインを使うことでページネーション周りをすっきりと書けています。

  • Jose Gonzalez’s Filter Plugin: これには私自身すごく助けられているが、君がモデルやそれに関連するモデルのデータを通してページネーションにフィルタリングをかけたい場合にだ。私は多くのリファクタリングをしたけど、まだまだ必要だよ。しかし、私が聞いたところによると結構プロダクションで使われているみたいで、少なくとも見た目上はうまく動いているようだね :)
  • CakeDCの検索プラグインと比べるととてもシンプルにページネーションが書けそうです。機能自体もシンプルです。標準のページネーションのロジックをまとめておきたいときなどに使えそうです。検索フォームを作るヘルパーもついていますね。

  • Neil Crooke’s Filter: このクソヤローは常に私のプラグインのアイディアを盗もうとしているんだ。まだこれは使ってないけどね。けどもしこれが彼の検索プラグインだとしたら、完全にやられたよ。あーぁ。
  • これも検索フォーム表示用のヘルパーがついています。URLについた検索パラメータを自動で取得し、ページネーションのパラメータにセットしてくれるようです。複合条件に対応、多くのオペレーション(イコールや大なり小なりなど)に対応していると書いてあります。

  • Neil Crooke’s Searchable: 検索インデックスのレコードにJSONを使えるプラグインだ。これはちょっとすごくて、私はCakeのパッケージに含めていくつかのサイトで使ってる。確実に見ておくべきものだね。
  • READMEがないので詳細は不明ですが、検索インデックスを作るシェルや検索インデックスモデルのためのビヘイビアが格納されていました。ソースのクエリを見る限り、全文検索用のプラグインなのでしょうか?

  • Kalt’s Search: 私が思うに、NeilはKaltからアイディアを盗んだんだ。多分ね。
  • 複数モデルに対応したCakePHPのためのサイト検索プラグインと銘打ってあります。こちらもMySQLの全文検索用のプラグインですね。全文検索のモードもしっかり選べるようです。テーブルを1つ追加して、そこにモデル名、ID、検索インデックスを仕込み、検索に使うようです。

  • Matt Curry’s Pagination Recall: このドキュメントのないプラグインはセッションにページネーションされた現在のページを保存出来るんだ。これはコントローラのページネーション対象のアクションにリダイレクトされたときでさえ、使えるよ。
  • 51行の短いコンポーネントですね。ページネーションのオプションをセッションに保存してくれるようです。

  • Matt Curry’s Yahoo BOSS: これを使えば、君のアプリケーションの中にYahoo! Boss検索を実装出来るよ。
  • Yahoo! BOSSがわからなかったのでググってみました。

    どうやら、Yahoo!からXMLやJSON形式で検索結果を得られるサービスのようです。
    特定のサイトに限っても検索結果を得られる、1日の検索制限はなし、とあるので、なかなか便利そうなサービスですね。
    bing移行後も継続しているようです。

File Uploading:

  • Vinicius Mendes’ MeioUpload: 私もこのプラグインには貢献したよ。バージョニングはちょっとあれだけど、これは確実に便利なプラグインだ。
  • ファイルをモデルに格納するためのビヘイビアです。テーブルを使わない方式も対応しているようですね。このプラグインを入れれば、ファイルアップロード周りのコードをほとんど書かなくて良さそうです。

  • Debuggable’s TransloadIt plugin: NodeJsベースのウェブサービスにファイルをアップロードするにはこのプラグインは最高だ。君のAjaxを通してエンコーディング、プロセッシングそしてストレージを提供してくれる。これはアツい。
  • Transloaditというサービスを利用するためのプラグインのようです。Transloaditを調べてみましたが、これは便利なサービスですね。アップロード時のプログレスバー表示や動画の変換、動画や画像のメタデータの生成までやってくれます。詳細はトップページを見るとわかりますよ。

  • Michał Szajbe’s UploadPack: アップロードしたファイルを出力するためのヘルパーだ。こいつはその用途じゃ、最高のもんだ。私は今じゃ、MeioUploadさえ超えたと思って、去年一年の間に何らかの形で、この二つのコードに貢献、そしてメンテナになっているよ。
  • ファイルアップロード用のヘルパーとビヘイビアのセットですね。サムネイルの表示などもヘルパーで出来るようです。確かにMeioUploadよりも機能が充実していそうです。

  • Jose Gonzalez’s Upload: 私は私が貢献したMeioUploadとUploadPackをベースにアップロードプラグインを作ったんだ。私はまだこれを使ってないけど、だいたい44%、ユニットテストされている。一度100%になったら試してみて、また君に教えるよ ;)
  • これはファイルアップロード用のビヘイビアですね。インターフェースがしっかりしていて使いやすそうな印象を受けます。MeioUploadやUploadPackとどう違うかは使ったことがないのでわかりませんが…。

  • David Perrson’s Media: 全てのCakePHPアップロードプラグインの先祖だ。君がもし、何かこのプラグインで必要以上のことをやりたいとしても、こいつはCakePHPプラグインとしてリリースされていないんだ。このプラグインで君がやりたいことが出来ないなら、こいつはCakePHPのプラグインじゃないね。ちょっと上級者向けのプラグインだ。けどがっかりしないでくれよ。
  • ここは訳がすごく微妙です。”CakePHPプラグインとしてリリースされてない”とあるのですが、どういうことでしょうか?内部がCakePHPのコードになっていない?カスタマイズしにくい、ってことかな。MASA-Pさんとcakephperさんにご指摘頂いて、上記の訳に直しました。うーむ、英語力が追いつきませんね。
    Media Pluginに関してはECWorks BlogMASA-Pさんの記事が参考になります。

普段、ページネーションは自前で、ファイルアップロードは使わないのですが、訳したことでとても興味が出てきました。

ファイルアップロードはTransloaditというサービスが気になります。
プラグインも用意されているので試しに使ってみようかな。トラフィック次第では動画共有サイトに使えそうですね。

検索に関してはYahoo! BOSSプラグインと全文検索のプラグインが気になりますね。
Yahoo! BOSSでの日本語検索はどの程度対応しているのでしょうか?bingになってどう変わったのか…。

そのうちこの2つのサービスを取り上げみましょうか。

次は”Optimization“と”Debugging“を訳しますよ。

[連載記事一覧]

  • 認証編
    • AuthComponentの代替えになるプラグインやfacebookと連携するプラグインが紹介されています。それに加え、使いにくいACLを使いやすくするツールも。個人的には一番、興味のある記事でした。
  • 最適化/デバッグ編
    • CakePHP 2に取り込まれるであろうプラグイン、そしてContainableBehaviorをさらに高機能にしたようなプラグインが紹介されています。DebugKitの使い勝手を向上するものもありますよ!
  • ヘルパー編
    • ブログを作るなら使いたいGravatarやGoogle APIをCakePHPのために取りまとめたプラグインが紹介されています。Google APIにCakePHPを対応させるプラグインは必見です。使った人がいたら、ぜひコメントください。

CakePHPでフィクスチャに惑わされずにテストを書く方法 – モック編

cakephp_testing.png

前回挙げたチュートリアルはやってみましたか?
快適なテストライフを送ってますか?

テストケースをたくさん書いていると気づくのは、フィクスチャがメンテナンスの邪魔をするということ。
フィクスチャに初期データを定義すると、それを気にしながらテストケースを作ることになります。
これがとても面倒くさいんです。

これを解消すべく、今日はモックを使ったテストケースの書き方を紹介します。

モックとは

SimpleTestのモックで参考になるのは、以下の書籍です。

Webアプリケーションテスト手法 Webアプリケーションテスト手法

  • 著者: 水野 貴明 (著), 石井 勇一 (著), 新藤 愛大 (著), 岸田 健一郎 (著), 荻野 淳也 (著), 安井 力 (著), 田中 慎司 (著)
  • 出版社: 毎日コミュニケーションズ
  • 発売日: 2008/7/25

この書籍のp154にモックについて以下のように書いてあります。

モックを使うとデータファイルからではなく擬似的に値を返せるので、OrderReaderがどのような動作をするのか可視的に図ることができます。もしCSVから違う入力形式をサポートするように仕様が変わった場合にも、このテストコードを見れば、修正が用意となります。

さてモックというと何だか難しく聞こえますが、テストコードの記述手順を箇条書きにしてみれば、それほどでもないと思うでしょう。モックを使わない場合のテストは以下の手順です。

1. テストするメソッドを呼び出す
2. テスト結果を評価する

一方でモックを使う場合は、以下の手順です。

1. モックを生成する
2. モックで戻り値を設定する
3. テストするメソッドを呼び出す
4. テスト結果を評価する
5. モックが使われたか確認する

“Webアプリケーションテスト手法”より

またモックを英英辞書で引くと以下のようにあります。

You use mock to describe something which is not real or genuine, but which is intended to be very similar to the real thing.

“Collins Cobuild English Dictionary”より

“very similar to real thing”がポイントですね。
つまり、一言でモックを言うならば”見せかけのクラス”です。

ランキングモデルのテストケースの修正

今回の記事は前回の”cakephpを使ったテスト駆動開発“の続きです。
使用したコードやチュートリアルの流れなどは全て前回の記事を参考にして下さい。

以下が前回、作成したランキングモデルのテストケースです。

/* Ranking Test cases generated on: 2010-07-13 18:07:16 : 1279014616*/
App::import('Model', 'Ranking');

// テストケース用のクラスはTestCaseで終わる名前にし、CakeTestCaseを継承する。TestCaseより前はファイル名と一致させる。必ずしもテストするクラス名と一致させる必要はない。
class RankingTestCase extends CakeTestCase {
  var $fixtures = array('app.ranking');

  function startTest() {
    $this->Ranking =& ClassRegistry::init('Ranking');
  }

  function endTest() {
    unset($this->Ranking);
    ClassRegistry::flush();
  }

  /**
   * addGoodメソッドのテスト
   */
  // testで始まるメソッドがテストとして実行される。テストメソッドは必ずtestでメソッド名を始めること。
  function testAddGood() {
    debug('addGoodメソッドのテスト');

    // 正常: id=1のデータに対してaddGoodメソッドを実行する。
    // 確認: 返り値がtrueであること、goodがプラス1されていること。
    $ret = $this->Ranking->addGood(1);
    $this->assertTrue($ret);
    $params = array(
      'conditions' => array('id = ' => 1),
      'fields' => array('good'),
      'recursive' => -1
    );
    $data = $this->Ranking->find('list', $params);
    $expected = array(1 => 2);
    // この部分がテスト: 期待する結果とメソッドの戻り値を比較して等しいならテストが通る。等しくないならテストが失敗する。
    $this->assertEqual($expected, $data);

    // 異常: id=2のデータに対してaddGoodメソッドを実行する。
    // 確認: 返り値がfalseであること。
    $ret = $this->Ranking->addGood(1);
    // この部分がテスト: メソッドの戻り値がfalseならテストが通る。false以外ならテストが失敗する。
    $this->assertFalse($ret);

  }

  /**
   * getGoodTitlesメソッドのテスト
   */
  // testで始まるメソッドがテストとして実行される。テストメソッドは必ずtestでメソッド名を始めること。
  function testGetGoodTitles() {

    // 正常: 取得件数を5件に設定し、getGoogTitlesメソッドを実行する。
    // 確認: 上位から5件のデータが取得出来ること。
    $ret = $this->Ranking->getGoogTitles(5);
    $expected = array(
      array(
        'Ranking' => array(
          'id' => 1,
          'title' => 'title1',
          'good' => 10,
        ),
      ),
      array(
        'Ranking' => array(
          'id' => 2,
          'title' => 'title1',
          'good' => 9,
        ),
      ),
      array(
        'Ranking' => array(
          'id' => 3,
          'title' => 'title1',
          'good' => 8,
        ),
      ),
      array(
        'Ranking' => array(
          'id' => 4,
          'title' => 'title1',
          'good' => 7,
        ),
      ),
      array(
        'Ranking' => array(
          'id' => 5,
          'title' => 'title1',
          'good' => 6,
        ),
      ),
    );
    // この部分がテスト: 期待する結果とメソッドの戻り値を比較して等しいならテストが通る。等しくないならテストが失敗する。
    $this->assertEqual($expected, $ret);

  }

}
?>

このうち、テスト対象のRankingモデルをモック化してしまいます。
こうすることで、特定のメソッドを実行したときに呼び出されるfindメソッドやdeleteメソッドのコール回数や引数を確認出来るのです。

テスト用のクラスRankingTestCaseを書く前に以下の記述を追加してください。

/**
 * テーブルを使用させないためにテスト対象のRankingモデルをオーバライド
 */
class TestRanking extends Ranking {
  var $useTable = false;
}

Mock::generatePartial(
  'TestRanking', 'MockRanking',
  array('exists', 'updateAll', 'find')
);

これでRankingモデルがモック化されたMockRankingモデルが生成されます。
モックの考え方は初め、意味がわからないと思います。
そのため、今回は説明の前にテストケースを書いてしまいます。

<?php
/* Ranking Test cases generated on: 2010-07-13 18:07:16 : 1279014616*/
App::import('Model', 'Ranking');

/**
 * テーブルを使用させないためにテスト対象のRankingモデルをオーバライド
 */
class TestRanking extends Ranking {
  var $useTable = false;
}

Mock::generatePartial(
  'TestRanking', 'MockRanking',
  array('exists', 'updateAll', 'find')
);

class RankingTestCase extends CakeTestCase {
    // TestRankingクラスを作ったこと、モッククラスを作ったことでテーブルが不要に。フィクスチャをコメントアウトする。
	// var $fixtures = array('app.ranking');

	function startTest() {
		$this->Ranking =& ClassRegistry::init('MockRanking');
	}

	function endTest() {
		unset($this->Ranking);
		ClassRegistry::flush();
	}

  /**
   * addGoodメソッドのテスト
   */
  function testAddGood() {
    debug('addGoodメソッドのテスト');

    // 正常: id=1のデータに対してaddGoodメソッドを実行する。
    // 確認: exists, updateAllメソッドが呼ばれていること。返り値がtrueであること。

    // existsメソッドが引数なしで呼ばれていること。
    $this->Ranking->expectOnce('exists', array());
    // existsメソッドの返り値をセット
    $this->Ranking->setReturnValue('exists', true);

    // updateAllメソッドがgoodをインクリメントする内容の引数で呼ばれていること。
    $fields = array('good' => 'good + 1');
    $conditions = array('id = ' => 1);
    $this->Ranking->expectOnce('updateAll', array($fields, $conditions));
    // updateAllメソッドの返り値をセット
    $this->Ranking->setReturnValue('updateAll', true);

    // addGoodメソッドの呼び出しと結果の確認
    $ret = $this->Ranking->addGood(1);
    $this->assertTrue($ret);

  }

  function testAddGoodNothing() {

    // 異常: 存在しないデータ(existsメソッドがfalseを返すデータ)に対してaddGoodメソッドを実行する。
    // 確認: existsメソッドが呼ばれていること。updateAllメソッドが呼ばれていないこと。 

    // existsメソッドが引数なしで呼ばれていること。
    $this->Ranking->expectOnce('exists', array());
    // existsメソッドの返り値をセット
    $this->Ranking->setReturnValue('exists', false);

    // updateAllメソッドが呼ばれていないこと。
    $this->Ranking->expectNever('updateAll');

    $ret = $this->Ranking->addGood(10);
    $this->assertFalse($ret);

  }

  /**
   * getGoodTitlesメソッドのテスト
   */
  function testGetGoodTitles() {
    debug('getGoodTitlesメソッドのテスト');

    // 正常: 取得件数を5件に設定し、getGoodTitlesメソッドを実行する。
    // 確認: findメソッドが呼ばれていること。

    // findメソッドが上位5件を取得する条件で呼ばれていること。
    $params = array(
      'limit' => 5,
      'page' => 1,
      'order' => 'good DESC',
      'recursive' => -1,
    );
    $this->Ranking->expectOnce('find', array('all', $params));
    $return = array(
      array(
        'Ranking' => array(
          'id' => 6,
          'title' => 'title6',
          'good' => 10,
        ),
      ),
      array(
        'Ranking' => array(
          'id' => 5,
          'title' => 'title5',
          'good' => 9,
        ),
      ),
      array(
        'Ranking' => array(
          'id' => 4,
          'title' => 'title4',
          'good' => 8,
        ),
      ),
      array(
        'Ranking' => array(
          'id' => 3,
          'title' => 'title3',
          'good' => 7,
        ),
      ),
      array(
        'Ranking' => array(
          'id' => 2,
          'title' => 'title2',
          'good' => 6,
        ),
      ),
    );
    $this->Ranking->setReturnValue('find', $return);

    $ret = $this->Ranking->getGoodTitles(5);
    $expected = array(
      array(
        'Ranking' => array(
          'id' => 6,
          'title' => 'title6',
          'good' => 10,
        ),
      ),
      array(
        'Ranking' => array(
          'id' => 5,
          'title' => 'title5',
          'good' => 9,
        ),
      ),
      array(
        'Ranking' => array(
          'id' => 4,
          'title' => 'title4',
          'good' => 8,
        ),
      ),
      array(
        'Ranking' => array(
          'id' => 3,
          'title' => 'title3',
          'good' => 7,
        ),
      ),
      array(
        'Ranking' => array(
          'id' => 2,
          'title' => 'title2',
          'good' => 6,
        ),
      ),
    );
    $this->assertEqual($expected, $ret); 

  }

}
?>

コメントに入れた観点でテストを実施しています。
前回のテストと今回のテストの観点の比較を以下に書いておきます。

前回のテスト 今回のテスト
1. テスト対象のメソッドを呼び、テーブルに実際にデータを挿入する。 1. テスト対象のメソッドを呼び、モック化されたクラスのメソッドを呼ぶ。
2. このデータをテストケースで読み込み、想定したデータと一致するか、確認する。 2. こうして呼ばれたメソッドの呼び出し回数や引数が一致するか、確認する。

まずはモックを使うと、”このようなテストが出来る“ということが理解出来ましたか?
これがわかれば後は書き方を理解するだけです。

モックの作り方

先ほどのコーディングで何をしているのか、モックはどう書くのかを順を追って説明して行きます。

まずはモックのイメージです。
始めに書いた以下のコードは、次のようなクラスを作成することを意味しています。

/**
 * テーブルを使用させないためにテスト対象のRankingモデルをオーバライド
 */
class TestRanking extends Ranking {
  var $useTable = false;
}

Mock::generatePartial(
  'TestRanking', 'MockRanking',
  array('exists', 'updateAll', 'find')
);

mock_object.png
モックのイメージ

このことは、Mock::generatePartialをprintすると簡単にわかります。

<?php
class MockRanking extends TestRanking {
  var $_mock;
  var $_mocked_methods = array('exists', 'updateall', 'find');

  function MockRanking() { $this->_mock = &new SimpleMock();
    $this->_mock->disableExpectationNameChecks();
  }

  function setReturnValue($method, $value, $args = false) {
    if (! in_array(strtolower($method), $this->_mocked_methods)) {
      trigger_error("Method [$method] is not mocked");
      $null = null;
      return $null;
    }
    $this->_mock->setReturnValue($method, $value, $args);
  }

  /** ----- 省略 ----- **/

  function exists() {
    $args = func_get_args();
    $result = &$this->_mock->_invoke("exists", $args);
    return $result;
  }  

  function updateAll() {
    $args = func_get_args();
    $result = &$this->_mock->_invoke("updateAll", $args);
    return $result;
  }  

  function find() {
    $args = func_get_args();
    $result = &$this->_mock->_invoke("find", $args);
    return $result;
  }

}
?>

わかりますか?つまり、Mockクラスがgenerateメソッドの呼び出しを受けて、自動的にコードを吐き出しているんです。
PHPはスクリプト言語なので、こうして吐き出されたコードも簡単に自身のコードの一部として扱うことが出来ます。
こういった面白いハックを見ると、自分でも作ってみたくなりますね。

話が少し逸れました。モックのクラスの生成について理解出来ましたか?
これが理解出来れば後は簡単、テストに使用する基本的なメソッドの動きがわかってくると思います。

後はもう自由にテストを書けますよね?

モックでよく使用するメソッド

最後にモックでよく使用するメソッドをまとめておきます。
もちろん、モック化したクラスに対してメソッドを実行してください。

このメソッド一覧があれば、どんなテストも簡単に書けるようになるでしょう。

メソッド名 動作
expectAt
($n, $method, $arguments)
n回目の特定のメソッドの呼び出しが指定した引数であることを確認する。
expectCallCount
($method, $n)
特定のメソッドがn回呼び出されることを確認する。
expectNever
($method)
特定のメソッドが呼び出されないことを確認する。
expectOnce
($method, $arguments)
呼び出し回数が1回で、かつ指定した引数であることを確認する。引数指定を省略した場合は、呼び出し回数のみ確認する。
setReturnValue
($method, $value)
特定のメソッドの返り値をセットする。
setReturnValueAt
($n, $method, $value)
特定のメソッドのn回目の返り値をセットする。

モック、なかなか便利でしょう。
私はよく、フィクスチャを書くのが面倒なときやデータベースの仕様が固まっていないときに、モデルのモックを作りテストします。

特にデータベースの仕様が固まっていないときに便利です。
テストケースを書きながら、テーブルのフィールドや型を考えることが出来ます。

これは以下の4ステップが、

  1. データベース仕様を決める。
  2. メソッドを作成する。
  3. データベース仕様を変更する。
  4. メソッドを変更する。

次の2ステップに変わることを意味します。

  1. メソッドを作成する。
  2. データベース仕様を決める。

なかなか、便利なので皆さんもやってみてください。

モックを使ったテストケースの書き方がわかりましたか?
もし、わからない点や間違った点があれば気軽にコメントをください。

[2010/09/08 追記]

Pythonでのテストの記事ですが、テストやメソッドを設計する上で参考になる点が多いと思います。
ご一緒にどうぞ。

CakePHPプラグインのまとめ – 認証編

cakephp_plugins.png

CakePHPのフォーラムで知ったのですが、Jose Diaz-Gonzalezさんがプラグインのまとめを書いてくれています。

CakePHPに限らずフレームワークの良いところ、それはやはりソースコードを再利用できることです。
そしてそのフレームワークの利用者が多ければ多いほど、再利用できるソースコードが増えていきます。

CakePHPにはビヘイビアやコンポートネント、ヘルパーから始まり、そして様々なものを統合して活用できるプラグインといったコードを再利用する仕組みが採用されています。
今回は先ほど紹介したブログの記事を日本語訳し、自分なりにまとめて、紹介します。

[注意] 初めてまともに訳すので、かなり意訳してます。ここはおかしい、変だ、という点があればご指摘ください。

認証と承認

  • Debuggable’s Authsome: AuthComponentを置き換えるのは悪いことじゃない。このコンポーネントは君のアプリ側でリダイレクトを操作できないからだ。その一方で、こいつはAuthsome::get('fieldName')というメソッドを持っていて、どこでも簡単にユーザデータを扱うことが出来るんだ。
  • “Auth for people who hate the Auth component”(AuthComponentが嫌いな人のための認証)というタイトルでgithubに公開していますね。
    AuthComponentはその構造上、カスタマイズが非常にしにくいです。
    AuthComponentとは違い、認証に関連する様々な設定を独自に定義出来る認証コンポーネントのようです。

  • Jose Gonzalez’s Sanction: 何か私が作るとき、AuthSomeとシンプルな設定ファイルでアプリケーションのパーミションを制御する。これはHtmlHelperの置き換えとして使うことができ、さらに君の設定ファイルをハックできるよ。
  • これを使うと、コンポーネントのオプションにアクセス許可するメソッドを指定出来るようです(AuthComponentはまだオプション指定出来ませんよね?)。
    さらに、ヘルパーが付属していて、これを使うと”認証が必要なページへのリンク“を自動的に非表示にする機能も備えているようです。
    かなり便利そうですね。

  • Nick Baker’s Facebook: Facebookをと連携するプラグインだ。AuthComponentと連携して使うことが出来る – 恐らく、Authsomeとも。Facebook認証部分のフル機能を作れるね。いつかはこれを使ってみたい。デモサイトはここ
  • AuthComponentや独自の認証システムと連携してfacebookの認証システムを構築出来る、とありますね。
    次のような機能を実装出来るようです。
    – Share (サイトをシェアする機能)
    – Like (良いね!機能)
    – Login/Logout (登録不要でfacebookユーザを認証する機能)
    – Activity (あなたのアプリケーションと友達のアクティビティを公開する機能)
    – Friend Pile (あなたのアプリケーションの友達を表示する機能)
    – Recommendations (現在のページのレコメンドを表示する機能)
    – Fan Boxes (あなたのアプリケーションのファンにする機能)
    – Profile Pictures (ユーザのプロフィール画像を表示する機能)
    – Live Streams (facebookを通して動的なライブストリーミングイベントを作り、あなたのサイト通してアクセスする機能)
    – Comments (facebookのコメントをあなたのサイトの好きな部分で利用する機能)
    – Status (ユーザのステータスを表示する機能)
    facebookの機能がわからないので微妙ですが、なんだか色々なことが出来そうです。
    これも使ってみたいものリスト入り。

  • Nick Baker’s Gigya: これはカスタマイズしたソーシャルネットワークプラグインのようだね。これを使うと君のアプリで1つのAPIを他のAPIと統合できるみたいだ。まだ成熟していないけど、確かに良いアイディアだ。
  • 何が出来るかはここを見るとわかりやすいですね。

    Gigyaというサービスを介してSNSをまとめてしまおうという試みのようです。
    Gigyaに対して”シェア”すれば、取りまとめたSNS全てに反映される、ということでしょうか?
    逆に自分で作ったサイトからGigyaに対して認証すれば、ユーザが持っているどれかのSNSで自動的に認証してくれると?
    これは、そのGigyaと連携するためのプラグインのようです。
    これも面白そうなサービスですね。

  • Jedt’s Spark Plug: AuthsomeとシンプルなACLで実装したとても便利なユーザ管理と管理者機能。私はまだ試してないけど、恐らくこれはパーミションのフィルタリングのために私の作ったコンポーネントを使ってるね。すぐにスクリーンショットを欲しいな。
  • READMEに詳しく載っていないので詳細は不明ですが、サンプルのconfig.phpを覗くと、かなりわかりやすくACLの設定が出来るようです。
    私はACL自体、あまり使ったことがないので微妙ですが、これも使いやすそうなプラグインです。

  • Valerij Bancer’s PoundCake Control Panel: プラグインの説明から: “動的にデータベースからユーザとグループにパーミションを割り当てるためのACLメニューを自動生成する、ユーザとグループ管理のためのパネル”、良いね。
  • これもREADMEが詳しく載っていないので、詳細は不明ですが、こんなスクリーンショットが置いてありました。
    screenshot.jpg
    これを自動で作れるならかなり素敵ですね。

  • Travis Rowland’s SuperAuth: もし君のサイトでローレベルなACLを実装したいなら、これはお勧めしない。こいつは本当に高機能で – テストは不足しているものの、本当に機能が充実している。テストが充実してたなら、本当にお勧めするよ。
  • 自動ログイン“の機能も備えているようです。
    パーミションのキャッシュ機能、クエリと一緒にパーミションを取得する機能、マルチグループ対応等々、本当に色々な機能を備えているようです。
    その一方、CakePHP 1.2でのテストやマルチグループのテストを行っていないと書いてあり、確かにテスト不足のようです。

  • Mark Story’s ACL Extras: とても扱いにくいACL。いつもACLを使えって?このシェルを使ったなら、君のACLライフを簡単にしてくれるよ。
  • 先ほど述べた通りACLを使ったことがないので、コメント出来ないのですが…ACLの面倒な設定をCLIで出来るのかな?

  • Mark Story’s Menu Component: ACLベースのメニューを作成してくれる。とてもクールじゃない?
  • ACLに基づいたメニューをコントローラをスキャンすることで自動で作ってくれるようです。
    メニューは配列としてビューにセットされるので、開発者はそれを表示するだけ、という訳です。
    きっちり構造化してあればかなり便利ですね。

  • Matt Curry’s Static User: このコードAuthsome::get('fieldName')を使うには、君はAuthComponentのコードを使いすぎてるよね?AuthComponentと同じことをするための簡単な方法だよ。ただし、User::get('fieldName')のような形だけど。とてもシンプルに実装できるから試してみて。
  • 認証系の処理をオーバーライドしてくれるイメージのようです。
    しかも、認証したユーザの情報をどこでも(ModelだろうがViewだろうが)取り出せるようにしてくれるんですね。
    うまく使わないと、コードがややこしくなりそうですが、これも便利そうです。

なかなか独特な表現を使っていて訳すのは難しいですね。
私の英語のレベルが低いからだとは思いますが…。

次はSearching and Paginationを訳しますね。

[連載記事一覧]

  • 検索とページネーション/ファイルアップロード編
    • CakePHPの使いにくいページネーションを使いやすくするプラグイン、Yahoo! BOSSのプラグインが紹介されています。ファイルアップロードはTransloaditというサービスに対応させるプラグインから、メジャーなメディアプラグインまで。
  • 最適化/デバッグ編
    • CakePHP 2に取り込まれるであろうプラグイン、そしてContainableBehaviorをさらに高機能にしたようなプラグインが紹介されています。DebugKitの使い勝手を向上するものもありますよ!
  • ヘルパー編
    • ブログを作るなら使いたいGravatarやGoogle APIをCakePHPのために取りまとめたプラグインが紹介されています。Google APIにCakePHPを対応させるプラグインは必見です。使った人がいたら、ぜひコメントください。

CakePHP 1.2.8 released

bakery.png

CakePHP1.2.8がリリースされました。
The Bakeryの記事の翻訳を載せておきます。

CakePHP 1.2.8 released

マイナーバージョンアップなので細かな修正が主ですね。

CakePHP 1.2.8 リリース

By Mark Story (mark_story)

CakePHP開発チームはCakePHP 1.2.8のリリースをアナウンスできてとてもハッピーです。1.2.8は1.2ブランチのメンテナンスリリースで1.2で、バグフィックスと最適化が含まれています。

4月に1.2.7をリリースしてから、30のコミットと20のチケットを処理しました。あなたのアプリケーションに少しだけ影響があるでしょう。

- l10nにおいてウェールズ語をサポートしました。
- Controller::validateErrorsが独立したモデルオブジェクトを許可するようになりました。詳しくはドキュメントに載せてあります。
- bindModelとunbindModelを複数回呼んだ場合に正しくアソシエーションをリセットするようになりました。
- TextHelper::autoLink()がURLに対してstrtolower関数を呼ばなくなりました。これは短縮URLに対応するためのものです。
- HttpSocketが同じオブジェクトに対してリクエストを発行した場合に認証を破棄しなくなりました。
- String::insert()は正しく、同じサブパターンで始まる2つのキーを扱えるようになりました。

もし全ての変更が知りたいならchangelog[2]とそれに関連するチケットをチェックすると良いでしょう。
1.3の開発とサポートが続けられています。2.0のブランチもまたいくつかのブランチと共に開発中です。
もし2.0の開発の手助けに興味があるなら、wikiのpages[3]を見て、チケットを投げてください。
私たちはリリースのためにチケット、パッチ、そしてドキュメントに貢献してくた皆さんに感謝したいです。
Cakefest 2010[4]もまた準備が着々と進んでいます。もしチケットをまだ買っていないなら、買えるうちに買ってください。

Download a packaged release [1]
View the changelog[2]

[1] http://github.com/cakephp/cakephp/downloads
[2] http://cakephp.lighthouseapp.com/projects/42648/changelog-1-2-8
[3] http://cakephp.lighthouseapp.com/projects/42648/milestones/71894-200
[4] http://cakefest.org

Controller::validateErrorsの修正

どのような修正か、わからなかったので少し調べました。

http://github.com/cakephp/cakephp/commit/418b8e5

どうやら、今まではコントローラにアタッチされていたモデルだけがvalidateErrorsの対象として選べたようです。
これたアタッチされていないモデルもvalidateErrorsの対象として選べるようになったようです。

訳が怪しいですが、頑張ってみました。皆さんのお役に立てれば幸いです。

CakePHPを使ったテスト駆動開発

ブログ初ポストはCakePHPを使ったテスト駆動開発です。

CakePHPはユニットテストとしてSimpleTestに対応しています。
SimpleTestをインストールするだけで、モデルやコントローラ、シェル、ルーティングクラスなどのユニットテストが出来るようになります。

今日はこのCakePHPとSimpleTestを使ってテスト駆動開発の流れを説明します。
ただ、僕自身テスト駆動開発を学んだのは去年のCake祭りなので、至らない点が多々あります。
もし何かあれば、コメントでご指摘ください。

今更感もありますが、この場を借りてCake祭りでテスト駆動の指導をしてくださった、@sizuhikoさんに感謝します。

開発手順

まずは開発手順を示します。少し細かいですが、テスト駆動では以下のような順で開発していきます。

  1. 設計する。
  2. テストケースを書く。
  3. テストケースをデバッグする。
  4. コードを書く。
  5. テストケースを実行する。
  6. コードをデバッグする。
  7. テストケースを全て通す
  8. コードが完成する。

コードとテストケースが分かれていること、それに始めにある程度設計してしまうのがポイントです。
設計といっても、私がやるのは白紙のA4用紙にクラスとメソッド名を書き出すぐらいです。
こうして置くと、テストケースもコードも書きやすくなります。

これだけ見ても、実感がないと思うので実例で説明していきます。

ランキングモデルの開発

良くある例としてCakePHPを使ったランキングを作ります。
といっても全てを作る時間はないので、ランキングデータを取り扱うランキングモデルをテスト駆動開発で作ります。

設計

モデル名やスキーマを次のようにします。

  • モデル名 : Ranking
    • addGood : goodを追加するメソッド
    • getGoodTitles : goodの順にデータを取得するメソッド
  • テーブル名 : rankings
    • id : 主キー
    • good : 良いと思った人の数
    • title : ブログのタイトル

ブログにgoodボタンをつけて、それを押すとスキーマのgoodが1増えるイメージです。
例としてはありがちですが、facebookの「良いね!」機能から考えました。

今回は書きませんが、コントローラからaddGoodメソッドやgetGoodTitlesメソッドを呼びます。

テストケース・コーディング

設計が決まったらテストケースを書いていきます。さっそく書き始めたいですが、その前に色々とやることがあります。
空の状態(CakePHPのappディレクトリ)からの開発を想定してますので、モデルやテーブルを作る必要があります。

まず、データベース設定をして、この画面が出るようにしましょう(設定の仕方は省略します)。

startup.jpg

次はschema.phpを作りましょう。schema.phpはCakePHPでデータベースのスキーマを管理出来るファイルです。
ここにスキーマ情報を書き込んでおけば、データベースのマイグレーションが簡単になります。

cake schema generate

Welcome to CakePHP v1.3.2 Console
---------------------------------------------------------------
App : ranking
Path: /home/tfmagician/ranking
---------------------------------------------------------------
Cake Schema Shell
---------------------------------------------------------------
Generating Schema...
Schema file: schema.php generated

schema.php

<?php
/* SVN FILE: $Id$ */
/* Ranking schema generated on: 2010-07-13 17:07:41 : 1279010981*/
class RankingSchema extends CakeSchema {
  var $name = 'Ranking';

  function before($event = array()) {
    return true;
  }

  function after($event = array()) {
  }

}
?>

まだ、データベーステーブルを作っていないので空の状態でテンプレートが出来上がります。
ここに設計したスキーマ情報をプロパティとして加えます。

  var $rankings = array(
    'id'    => array('type' => 'integer',  'null' => false, 'default' => NULL, 'key' => 'primary'),
    'title' => array('type' => 'string',   'null' => false, 'default' => NULL, 'key' => 'index'),
    'good'  => array('type' => 'integer',  'null' => false, 'default' => 0),
    'indexes' => array(
      'PRIMARY' => array('column' => 'id',    'unique' => 1),
      'title'   => array('column' => 'title', 'unique' => 0),
    ),
    'tableParameters' => array(
      'charset' => 'utf8',
      'collate' => 'utf8_general_ci',
      'engine'  => 'MyISAM',
    ),
  );

これを元にCakePHPにテーブルを作らせます。

cake schema create

Welcome to CakePHP v1.3.2 Console
---------------------------------------------------------------
App : ranking
Path: /home/tfmagician/ranking
---------------------------------------------------------------
Cake Schema Shell
---------------------------------------------------------------

The following table(s) will be dropped.
rankings
Are you sure you want to drop the table(s)? (y/n)
[n] > y
Dropping table(s).
rankings updated.

The following table(s) will be created.
rankings
Are you sure you want to create the table(s)? (y/n)
[y] >
Creating table(s).
rankings updated.
End create.

これで指定したスキーマのテーブルが出来ているはずです。phpmyadminなどで確認しておきましょう。

次にテストで使うコードとモデルのコードをbakeで作成します。

cake bake model

Welcome to CakePHP v1.3.2 Console
---------------------------------------------------------------
App : ranking
Path: /home/tfmagician/ranking
---------------------------------------------------------------
---------------------------------------------------------------
Bake Model
Path: /home/tfmagician/ranking/models/
---------------------------------------------------------------
Possible Models based on your current database:
1. Ranking
Enter a number from the list above,
type in the name of another model, or 'q' to exit
[q] > 1
Would you like to supply validation criteria
for the fields in your model? (y/n)
[y] > y

Field: id
Type: integer
---------------------------------------------------------------
Please select one of the following validation options:
---------------------------------------------------------------
1 - alphanumeric
2 - between
3 - blank
4 - boolean
5 - cc
6 - comparison
7 - custom
8 - date
9 - decimal
10 - email
11 - equalto
12 - extension
13 - inlist
14 - ip
15 - maxlength
16 - minlength
17 - money
18 - multiple
19 - notempty
20 - numeric
21 - phone
22 - postal
23 - range
24 - ssn
25 - time
26 - url
27 - userdefined
28 - Do not do any validation on this field.
... or enter in a valid regex validation string.

[28] >

Field: title
Type: string
---------------------------------------------------------------
Please select one of the following validation options:
---------------------------------------------------------------
1 - alphanumeric
2 - between
3 - blank
4 - boolean
5 - cc
6 - comparison
7 - custom
8 - date
9 - decimal
10 - email
11 - equalto
12 - extension
13 - inlist
14 - ip
15 - maxlength
16 - minlength
17 - money
18 - multiple
19 - notempty
20 - numeric
21 - phone
22 - postal
23 - range
24 - ssn
25 - time
26 - url
27 - userdefined
28 - Do not do any validation on this field.
... or enter in a valid regex validation string.

[19] >
Would you like to add another validation rule? (y/n)
[n] >

Field: good
Type: integer
---------------------------------------------------------------
Please select one of the following validation options:
---------------------------------------------------------------
1 - alphanumeric
2 - between
3 - blank
4 - boolean
5 - cc
6 - comparison
7 - custom
8 - date
9 - decimal
10 - email
11 - equalto
12 - extension
13 - inlist
14 - ip
15 - maxlength
16 - minlength
17 - money
18 - multiple
19 - notempty
20 - numeric
21 - phone
22 - postal
23 - range
24 - ssn
25 - time
26 - url
27 - userdefined
28 - Do not do any validation on this field.
... or enter in a valid regex validation string.

[20] >
Would you like to add another validation rule? (y/n)
[n] >
Would you like to define model associations
(hasMany, hasOne, belongsTo, etc.)? (y/n)
[y] >
One moment while the associations are detected.
---------------------------------------------------------------
Please confirm the following associations:
---------------------------------------------------------------
Would you like to define some additional model associations? (y/n)
[n] >

---------------------------------------------------------------
The following Model will be created:
---------------------------------------------------------------
Name:       Ranking
DB Table:   `rankings`
Validation: Array
(
    [title] => Array
        (
            [notempty] => notempty
        )

    [good] => Array
        (
            [numeric] => numeric
        )

)

Associations:
---------------------------------------------------------------
Look okay? (y/n)
[y] >

Baking model class for Ranking...

Creating file /home/tfmagician/ranking/models/ranking.php
Wrote `/home/tfmagician/ranking/models/ranking.php`
SimpleTest is not installed. Do you want to bake unit test files anyway? (y/n)
[y] > y

You can download SimpleTest from http://simpletest.org

Baking test fixture for Ranking...

Creating file /home/tfmagician/ranking/tests/fixtures/ranking_fixture.php
Wrote `/home/tfmagician/ranking/tests/fixtures/ranking_fixture.php`
Bake is detecting possible fixtures..

Creating file /home/tfmagician/ranking/tests/cases/models/ranking.test.php
Wrote `/home/tfmagician/ranking/tests/cases/models/ranking.test.php`

(あ、忘れていましたが、cakeコマンドはcakeコンソール(cake/console/cake)のことです。適宜置き換えてくださいね。)

これでテストケースとフィクスチャ(後で説明します)、モデルのテンプレートが完成しました。
あとはコーディングしていくだけです。

と、一つ忘れていたことがありました。SimpleTestのインストールです。
SimpleTestはアプリケーションのvendorsか、CakePHP本体と同じ階層のvendorsに配置します。

cd vendors/
wget http://downloads.sourceforge.net/simpletest/simpletest_1.0.1.tar.gz
tar zxvf simpletest_1.0.1.tar.gz
rm simpletest_1.0.1.tar.gz

配置したら、ブラウザからtest.phpにアクセスします。

test.php
test_php.jpg

左メニューからApp > Test Casesをクリック
test_cases.jpg

コンテンツからmodels / Rankingをクリック
ranking_test.jpg

bakeで作ったテンプレートのテストが実行されているのがわかります。
まだ何も書いていないですが、テンプレートのテストが実行され、1件のテストケースが通っています。

これでテストが実行出来るようになりました。テストケースをコーディングします。
テストケースはさきほどのbakeの最後に書かれている通り、tests/cases/models.ranking.test.phpにあります。
これを元に、オリジナルのテストケースをコーディングします。

tests/cases/ranking.test.php

/* Ranking Test cases generated on: 2010-07-13 18:07:16 : 1279014616*/
App::import('Model', 'Ranking');

// テストケース用のクラスはTestCaseで終わる名前にし、CakeTestCaseを継承する。TestCaseより前はファイル名と一致させる。必ずしもテストするクラス名と一致させる必要はない。
class RankingTestCase extends CakeTestCase {
  var $fixtures = array('app.ranking');

  function startTest() {
    $this->Ranking =& ClassRegistry::init('Ranking');
  }

  function endTest() {
    unset($this->Ranking);
    ClassRegistry::flush();
  }

  /**
   * addGoodメソッドのテスト
   */
  // testで始まるメソッドがテストとして実行される。テストメソッドは必ずtestでメソッド名を始めること。
  function testAddGood() {
    debug('addGoodメソッドのテスト');

    // 正常: id=1のデータに対してaddGoodメソッドを実行する。
    // 確認: 返り値がtrueであること、goodがプラス1されていること。
    $ret = $this->Ranking->addGood(1);
    $this->assertTrue($ret);
    $params = array(
      'conditions' => array('id = ' => 1),
      'fields' => array('good'),
      'recursive' => -1
    );
    $data = $this->Ranking->find('list', $params);
    $expected = array(1 => 2);
    // この部分がテスト: 期待する結果とメソッドの戻り値を比較して等しいならテストが通る。等しくないならテストが失敗する。
    $this->assertEqual($expected, $data);

    // 異常: id=2のデータに対してaddGoodメソッドを実行する。
    // 確認: 返り値がfalseであること。
    $ret = $this->Ranking->addGood(1);
    // この部分がテスト: メソッドの戻り値がfalseならテストが通る。false以外ならテストが失敗する。
    $this->assertFalse($ret);

  }

  /**
   * getGoodTitlesメソッドのテスト
   */
  // testで始まるメソッドがテストとして実行される。テストメソッドは必ずtestでメソッド名を始めること。
  function testGetGoodTitles() {

    // 正常: 取得件数を5件に設定し、getGoogTitlesメソッドを実行する。
    // 確認: 上位から5件のデータが取得出来ること。
    $ret = $this->Ranking->getGoogTitles(5);
    $expected = array(
      array(
        'Ranking' => array(
          'id' => 1,
          'title' => 'title1',
          'good' => 10,
        ),
      ),
      array(
        'Ranking' => array(
          'id' => 2,
          'title' => 'title1',
          'good' => 9,
        ),
      ),
      array(
        'Ranking' => array(
          'id' => 3,
          'title' => 'title1',
          'good' => 8,
        ),
      ),
      array(
        'Ranking' => array(
          'id' => 4,
          'title' => 'title1',
          'good' => 7,
        ),
      ),
      array(
        'Ranking' => array(
          'id' => 5,
          'title' => 'title1',
          'good' => 6,
        ),
      ),
    );
    // この部分がテスト: 期待する結果とメソッドの戻り値を比較して等しいならテストが通る。等しくないならテストが失敗する。
    $this->assertEqual($expected, $ret);

  }

}
?>

コメント中にも書いてありますが、assertメソッドがテストです。
例えばassertEqualメソッドは、「2つの変数が等しいことを期待する」という意味になります。
この期待に応えない(変数が等しくない)場合、テストは失敗します。後でスクリーンショットで示します。

またテストメソッドが実行される順は、次のようになります。

  1. startTest
  2. テストメソッド1(testで始まるメソッド)
  3. endTest
  4. startTest
  5. テストメソッド2(testで始まるメソッド)
  6. endTest

なのでこのテストケースでは、以下の順でテストが実行されます。

  1. startTest
  2. testAddGood
  3. endTest
  4. startTest
  5. testGetGoodTitles
  6. endTest

必ずstartTestとendTestメソッドが呼ばれる点がポイントです。これらのメソッドでクラスの初期化などの処理を実施し、テストを書きやすくします。

このテストではデータの挿入、更新を行っています。しかし、さきほどテーブルを作ったばかりなのでデータはまだ入っていないはずです。
逐一、テストの度に手作業でデータを入れてもいいのですが、それはとても面倒です。そこでフィクスチャという機能を使います。

CakePHPのフィクスチャ機能はテーブルのデータをPHPのコードとして書いておくと、勝手にテスト前にデータを挿入してくれます。
挿入のタイミングはテストメソッド(テストケースの中のtestで始まるメソッド)毎です。つまり、特定のテストメソッドでデータを更新したり削除しても、次のテストメソッドでは新しいデータが入っています。
これを知っておくと、テストケースを書くのが楽になります。

tests/fixtures/ranking_fixture.php

/* Ranking Fixture generated on: 2010-07-13 18:07:16 : 1279014616 */
class RankingFixture extends CakeTestFixture {
  var $name = 'Ranking';

  // スキーマ情報をfieldsプロパティとして書いておきます。indixesはインデックスの指定、tableParametersはテーブルの設定値です。schema.phpを同じ形式で指定します。
  var $fields = array(
    'id' => array('type' => 'integer', 'null' => false, 'default' => NULL, 'key' => 'primary'),
    'title' => array('type' => 'string', 'null' => false, 'default' => NULL, 'key' => 'index'),
    'good' => array('type' => 'integer', 'null' => false, 'default' => '0'),
    'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1), 'title' => array('column' => 'title', 'unique' => 0)),
    'tableParameters' => array('charset' => 'utf8', 'collate' => 'utf8_general_ci', 'engine' => 'MyISAM')
  );

  // データをrecordsプロパティとして書いておきます。
  var $records = array(
    array(
      'id' => 1,
      'title' => 'title1',
      'good' => 5,
    ),
    array(
      'id' => 2,
      'title' => 'title2',
      'good' => 6,
    ),
    array(
      'id' => 3,
      'title' => 'title3',
      'good' => 7,
    ),
    array(
      'id' => 4,
      'title' => 'title4',
      'good' => 8,
    ),
    array(
      'id' => 5,
      'title' => 'title5',
      'good' => 9,
    ),
    array(
      'id' => 6,
      'title' => 'title6',
      'good' => 10,
    ),
  );
}
?>

フィクスチャの書き方は省略します。ただ、連想配列で指定するので少し考えれば意味はわかるかと思います。
書き方がわからない場合はschema.phpを参考にしてください。

テストケースのデバッグ

作成したテストケースをデバッグします。まだモデル側のコードは書いていませんが、まずはtest.phpを開いてみましょう。

test.php
ranking.jpg

メソッドをまだ用意していないため、モデルにクエリを発行されてしまっています(CakePHPでは存在しないメソッドは全てクエリとして扱われます)。
これではデバッグにならないので、モデルにメソッドだけ用意しましょう。

models/ranking.php

<?php
class Ranking extends AppModel {
  var $name = 'Ranking';
  var $displayField = 'title';
  var $validate = array(
    'title' => array(
      'notempty' => array(
        'rule' => array('notempty'),
        //'message' => 'Your custom message here',
        //'allowEmpty' => false,
        //'required' => false,
        //'last' => false, // Stop validation after this rule
        //'on' => 'create', // Limit validation to 'create' or 'update' operations
      ),
    ),
    'good' => array(
      'numeric' => array(
        'rule' => array('numeric'),
        //'message' => 'Your custom message here',
        //'allowEmpty' => false,
        //'required' => false,
        //'last' => false, // Stop validation after this rule
        //'on' => 'create', // Limit validation to 'create' or 'update' operations
      ),
    ),
  );

  function addGood($id) {
  }

  function getGoodTitles($n) {
  }
}
?>

再度、test.phpを開きます。

test.php
test_debug.jpg

そうすると、一つだけクエリが発行されているのがわかります。
すでにお気づきの方がいるかもしれません。そうです、テストケースで実行しているRankigクラスのメソッド名がgoogTitlesとなっているのです。
よくあるtypoですね。修正し、再度テストケースを実行します。

test.php
test_debug_passed.jpg

これでテストケースのコードは大丈夫のようです。この段階でテストは通っていなくとも大丈夫です。
なんせ、まだ本体のコードを書いていないのですから。

コーディング

それでは本体のコードを書いていきます。先ほど追加した空のメソッドに以下のようにコーディングします。

models/ranking.php

  function addGood($id) {
    $this->id = $id;
    if(!$this->exists()) {
      return false;
    }
    return $this->updateAll(array('good' => 'good + 1'), array('id = ' => $id));
  }

  function getGoodTitles($n) {
    $params = array(
      'limit' => $n,
      'page' => 1,
      'order' => 'good DESC',
      'recursive' => -1,
    );
    return $this->find('all', $params);
  }

コーディングが完了したらtest.phpのページを再度開きます。

test.php
test_passed.jpg

テストが通ると、テストした件数と緑色のバーが表示されます。
この緑色のバーと作成したテスト件数があっていれば、テストは完全に通っています。

ここではあっさりテストを通してしまいましたが、実は1度のコーディングでテストが完了した訳ではありません。
この程度のコードでも2〜3度、テストを失敗し、コードを書き直しています。

やってみるとわかるのですが、テストケースがあることで簡単に、かつ手軽にコードを書き換えることができます。

テストケースを使ったテスト駆動開発を知るまでは、var_dumpを活用し、逐一途中のデータを見ながらコードを書いていました。
しかし、この方法だと仕様が自分の頭の中にしかないため、メソッドの機能が多様化してしまったり、想定しないバグに出くわしたりします。

テストケースを書くことで、頭の中にあるメソッドの仕様をコードとして書き出すことになります。
そうすることで、曖昧だった点や考慮しなければいけないケースも浮き彫りとなります。
さらにコードとして書き出すことで、メソッドを作る上での目標が定まり、コーディングしやすくなります。

テストケースというコードが増えるため、これを書くのを面倒に思ってしまいますが、この面倒さ以上のメリットがテスト駆動開発にはあると私は思います。

テスト駆動開発をする上でのポイント

最後に、テスト駆動開発でのポイントを書いておきます。参考にしてください。

  • テストケースを必ず始めに書く。
    • 後から書こうと思うと面倒くさくなる。また、後から書くと先ほど書いたメリットを受諾出来ない。
  • メソッド、クラスの機能追加時も必ずテストケースを書く。
    • テストケースも更新していかないと、せっかく書いたコードが無駄になる。テストケースはコードがしっかり動いていることの証拠である。
  • バグを見つけたときこそ、必ずテストケースを書く。
    • テストケースは上記と同様にコードがしっかり動いていることの保証である。
  • バグを見つけたときは、テストケースでしっかり対策する。
    • 統合テストでバグを見つけた場合はデバッグし、原因を見つける。その原因をメソッドレベルにまで落とし、そのメソッドのテストケースを書き、解決する。従ってテストケースも独立していることが重要である。
  • テストケースを出来るだけ独立させる。
    • 対象のメソッド、クラス以外の部分で仕様変更があった場合に、テストが通らなくなるのはテストケースのメンテナンスが面倒になる。出来るだけテストケースの対象を1つのメソッド、あるはクラスにしぼる。

これまでテスト駆動開発をやってきて学んだことを上げてみました。
テスト駆動開発で重要なことは一言で言うとメンテナンスしやすいテストケースを書くことだと思います。
テスト駆動開発をこれから始める方は、この点に注意すると、私の二の舞*1にならないかと思います。

ご参考になれば幸いです。

*1 テスト駆動開発を始めた頃は良かったのだが、その後メンテナンスが大変になった。結局、テストケースを全て捨てることに…。

ホーム > PHP > CakePHP

スポンサードリンク
書いている人
つぶやき
RSS 気になるニュース
過去の記事

ページの上部に戻る