Technote

by sizuhiko

CakePHPのデータジェネレータ Fabricate を作りました

<!– more –>Fabricate は Ruby の Fabricator に影響を受けたもので、データを簡単にすばやく生成する CakePHP2 用のプラグインです。

GitHub: sizuhiko/Fabricate

動作確認しているバージョンはCakePHP2.3.10, CakePHP2.4.1ですが、他のバージョンでもCakePHP2系なら動くと思います。

インストール方法

Composer対応しているので、composer.json に追加するだけで大丈夫です。テストだけに利用するなら require-dev に、実行コードからも使いたい場合は require に追加してください。どちらでも動作します。

    "extra": {
        "installer-paths": {
            "Plugin/Fabricate": ["sizuhiko/fabricate"]
        }
    },
    "require": {
        "composer/installers": "*",
        "sizuhiko/fabricate": "*"
    }

composer/installers に対応していますので、お好みで extra/installer-paths に追加すれば、Plugin の下に良い感じでインストール可能です。

後は bootstrap.php でプラグインの宣言を追加してください。

CakePlugin::load('Fabricate');

概要

Fabricateプラグインには3つのメソッドがあります。

  • Fabricate::attributesfor(:modelname, :numberofgeneration, :callback) : CakePHPの属性配列をデータ付きで返します(複数件生成可能)
  • Fabricate::build(:model_name, :callback) : CakePHPのモデルインスタンスを返します(1件のみ生成可能)
  • Fabricate::create(:modelname, :numberof_generation, :callback) : 指定された件数分データベースに生成する

用途

このプラグインは以下のような用途にとても向いています。

  • ページングのテストをするのに大量にデータを生成したい
  • fixture だと各テストケースで同じデータが生成されてしまうけど、各テストケース毎に違うデータを生成したい。かつテスト対象のカラム以外は適当な値が自動的に入って欲しい
  • Bddプラグイン(CakePHPのBehatインテグレーションプラグイン)のステップで「Postモデルに100件のデータが登録されていること」みたいなのを書きたいのに、簡単に記述できないなぁと思ったとき

使い方&サンプル

attributes_for

$results = Fabricate::attributes_for('Post', 10, function($data){
    return ["created" => "2013-10-09 12:40:28", "updated" => "2013-10-09 12:40:28"];
});

// $results is followings :
array (
  0 => 
  array (
    'id' => 1,
    'title' => 'Lorem ipsum dolor sit amet',
    'body' => 'Lorem ipsum dolor sit amet, aliquet feugiat. Convallis morbi fringilla gravida, phasellus feugiat dapibus velit nunc, pulvinar eget sollicitudin venenatis cum nullam, vivamus ut a sed, mollitia lectus. Nulla vestibulum massa neque ut et, id hendrerit sit, feugiat in taciti enim proin nibh, tempor dignissim, rhoncus duis vestibulum nunc mattis convallis.',
    'created' => '2013-10-09 12:40:28',
    'updated' => '2013-10-09 12:40:28',
  ),
  1 => 
  array (
  ....

これでPostモデル(第一引数に名前を指定)のスキーマ情報からカラム(属性)を取得して、タイプに応じた値が自動的に生成されて配列で戻ります。
第二引数に生成するレコード件数を指定します。
ただ、全部自動的だと困るという場合があるので、3つ目の引数にコールバック関数を指定可能になっています。このコールバック関数は、10件のデータを生成する場合、都度計10回呼び出されます。
戻り値として、属性名をキーに値を持つ連想配列を指定すると、都度array_mergeが実行されます(recursiveではありません)。
データの自動生成処理は、CakePHPのコアにあった FixtureTask の _generateRecords() というメソッドを拝借してきて、そのまま利用しています。
CakePHPでfixtureを生成したときrecordsに入っている内容と同じ物になります。

このメソッドの使いどころとしては、関連先との関連も生成して saveAll したいといったような、ちょっと応用的な使い方に向いていると思います。

また、第二引数、第三引数は省略可能ですので、

// 1件だけ生成
Fabricate::attributes_for('Post');
// 1件だけコールバックを指定して生成
Fabricate::attributes_for('Post', function($data){
    return ["created" => "2013-10-09 12:40:28", "updated" => "2013-10-09 12:40:28"];
});
// 10件生成
Fabricate::attributes_for('Post', 10);

というバリエーションでも利用可能です。

build

とりあえず本家のFabricatorに近づけたたくて作ったメソッドですが、CakePHPではモデルのインスタンスを複数同時に持てないので、正直使い勝手としては微妙です(作者が言うなという話ですが)。

create

最も簡単にデータをデータベースに保存するなら、このメソッドで十分です。
attributesforで作ったレコードをすべてデータベースに保存します。
引数の使い方はattributes
forと同一になっています。

きっかけと今後

ページングのテストを書こうとして、とにかく面倒になってきて「何でCakePHPにはFabricatorがないんだー」というノリで、カッとなって作った系ですので、機能的には最低限になっています。
かつたぶん最新のFabricatorのクローンとも言い難い状況です。

またカッとなったら、最新のFabricatorの機能に近づけようかな〜、どなかた使っていただける方がいればご要望、機能追加は GitHubのIssue、Pull Requestでお願いします。

CakePHPのコントローラテストで注意すること

コントローラのテストは難解である

とは言え、テストを書かないというのも何なのでテストを書くわけですが。

CakePHPではControllerTestCaseというテストケースクラスを継承してテストケースを書くのですが、Cakeのテストにモックが導入される前は testAction のオプションに PostsTestController のような PostsController を継承したクラスを作成し、内部的にそれを利用するように渡していました。

class PostsTestController extends PostsController {
// モックしたい処理
....
}
class PostsControllerTest extends ControllerTestCase {

    public function testアクションをテストする() {
        $this->testAction('url', array('controller'=>'PostsTestController'));
    }
}

ただし現在のCakePHP 2.3や2.4系では、モックオブジェクトを利用します。

なぜモックが必要?

もしこの記事を読んでいる方で、testActionがブラウザからも、コンソールからも成功するよ、という場合は依存関係が少ないか、たまたまなのかもしれません。

Webアプリケーションのコントローラは様々な依存関係があり、簡単にテストを通過させることができません。特にSessionComponentを使ったactionをテストする場合、問題が発生します。
Sessionはブラウザからテストを実行している範囲では問題が起きないのですが、コンソールで実行するとエラーになるという事があります。まぁコンソールにはセッションなんてないからね。当然です。

しかし実装上はセッションから値を取り出して何か処理をするという事がありえるわけです。

// 実際のコントローラ
    $components = ['Session', 'Auth'];

    public function index() {
        $hoge = $this->Session->read('hoge');
        .....
    }

// テストケース
    public function testIndex() {
        CakeSession::write('hoge', 'fuga');
        $this->testAction('/posts/index');
        ......
    }

みたいなコードがあった場合にindexアクションをテストすると、ブラウザからは成功するのですが、コンソールからだと失敗する。経験したことありませんか?

どうやってモックを使うのか

で、調べてみたら意外と解説してある部分がない。こういうときはCakePHPのコアテストコードを見るのが一番です。
具体的には lib/Cake/Test/Case/TestSuite/ControllerTestCaseTest.php です(lib/Cake/Test/Case/Controller/…でないところがミソ)。

で、上記コードを参照した上で先ほどのテストコードを改修すると、以下のようになります。

    public function testIndex() {
        $this->controller = $this->generate("Posts", ['components' => ['Session']]);
        $this->controller->Session->expects($this->any())
             ->method('read')
             ->will($this->returnValueMap([['hoge', 'fuga']]));
        $this->testAction('/posts/index');

$this->generate() はControllerTestCaseが定義しているモックを作成するためのメソッドで、第一引数は PostsController の Controller 部分を除いた名前です。第二引数には components や helper、model など、コントローラから依存関係にあるモジュールを同時にモックしたい場合に配列形式で記述します。今回はSessionコンポーネントをモックしたいので、第二引数に指定します。
このgenerate()はコントローラの初期化や依存関係のモック処理もやってくれるとても強力な機能を持っています。PHPUnitのgetMockでモックするのではなく、必ずこのメソッドを使うようにしましょう。

generate()の戻り値はコントローラのモックオブジェクトになっています。さらにSessionもモックしているので、 $this->controller->Session に対して、read(‘hoge’)はいつでも'fuga'を返すという記述をすることが可能です。
モック自体はPHPUnitのモックオブジェクトなので、詳しい記法はPHPUnitのマニュアルからモックオブジェクトの章を参照してください。

PHPUnit3.7のモックオブジェクト解説

最後に

コントローラのテストは本当に難解です。ただコアのテストコードを見るだけでもだいぶ理解が深まります。一度自分のアプリでテストを書く前に、その親クラス(ModelとかControllerなど)やテストケースのテストコードを見ると、これまでにテストの書き方がわからなかったところも、腑に落ちることがあるはずです。

CakePHP BddPlugin updates (2013/2/19)

v0.9.3リリース

本日バージョン0.9.3をリリースしました。

機能面での修正というわけではなく、今回は Composer の A Multi-Framework Composer Library Installer (https://github.com/composer/installers) という仕組みに対応したものです。

なのでpluginsディレクトリでgit cloneして引き続きご利用の皆様は、今回のアップデートは無視してもらっても大丈夫です。

なにが嬉しいのか

Composerのフレームワークプラグインを管理する機能については、ちょっと前から知っていたのですが、まぁCakePHPでは3以降中心かな?と思っていたところ @abe4tawa8 さんから Pull Request をいただきまして、修正を始めたといういきさつです。

このComposerの仕組みを使うと、appディレクトリの下に利用するプラグインとかPHPUnitとかのrequireを指定したcomposer.jsonを置いておくことでプロジェクトで必要なモジュールの依存関係を解決しようというものです。

例えば

{
    "config": {
        "vendor-dir": "Vendor"
    },
    "minimum-stability": "dev",
    "repositories": [
        {
            "type": "pear",
            "url": "http://pear.phpunit.de"
        },

        {
            "type": "vcs",
            "url": "https://github.com/sizuhiko/CommonContexts.git"
        },
        {
            "type": "vcs",
            "url": "https://github.com/sizuhiko/Spec-PHP.git"
        },

        {
            "type": "vcs",
            "url": "git://github.com/sizuhiko/Bdd.git"
        }
    ],
    "require-dev": {
        "sizuhiko/Bdd": "dev-develop",
        "behat/mink-goutte-driver": "*",
        "behat/mink-selenium-driver": "*",
        "behat/mink-selenium2-driver": "*"
    }
}

のように書くと、Bddプラグインを app/Plugins ディレクトリにインストールすることが可能になります。

あとは app/Config/bootstrap.php に

CakePlugin::load('Bdd');
require dirname(dirname(__FILE__)).DS.'Vendor/autoload.php';

BddPluginの利用と、依存関係のライブラリ群のautoloadをrequireすれば大丈夫です。

残念なお知らせ

Composerでは依存先のライブラリのrepositories指定を参照してくれません。

Repositories are not resolved recursively. You can only add them to your main composer.json. Repository declarations of dependencies’ composer.jsons are ignored.

http://getcomposer.org/doc/04-schema.md#repositories より

BddPluginではPullRequest中のCommonContextやforkしてカスタマイズしているSpec-PHPを使っており、それらのリポジトリを指定する必要があります。

他の依存関係はどうなっているの?と、気付いた方はするどいわけですが、ComposerはPackagistというリポジトリサイトを使っており、ここに必要なリポジトリを追加すれば良い訳です。登録は無料です。

で、これらforkしたプロジェクトについても登録してしまえば良いと思って作業を進めていたのですが、いざ手元でも動作確認がほぼOKになったところでPackagistのサイトでSubmitしようとすると

Do not submit forks of existing packages. If you need to test changes to a package that you forked to patch, use VCS Repositories instead. If however it is a real long-term fork you intend on maintaining feel free to submit it.

特に「*Do not submit forks of existing packages. *」の部分は太字になっていまして、他を受け付けない雰囲気を醸し出しています。まぁ要はメンテナンスを続けるならフォークしたプロジェクトでも登録可能だよ、と書いてあるわけですが、あまりの警告にビビってしまい各アプリケーションのcomposer.jsonにrepositoriesを追加してもらう方針としました。必要な部分は上記サンプルのとおりになっています。

さいごに

そういえばBddPlugin自体はPackagistに登録しても問題ないのだった!、と今Blogを書いていて気がついたので、それはこれから実施します。そうするとBddPluginのリポジトリ宣言は必要なくなりますね(汗

今のcomposer.jsonではnameがsizuhiko/Bddになっていて、BddをスモールケースにしろとPackagistに怒られているので、少々時間がかかります

それと先日Co-Edoで行われたCakeBeerTalkの飛び入りLTでも話しましたが、使っていての不明点など何でもgithubのissueで質問ください。よろしくおねがいします。

BddPluginを使った勉強会もやりたいですなー

CakePHP BddPlugin updates

v0.9.2リリース

CakePHPのBddプラグインも少しずつ浸透し、国内外より利用者様の声が届いて、いくつか不具合&機能改善などを取り込んできました。

本日リリースしたv0.9.2としては、数回にわたり@kaz_29さんと、もくもく会を重ねた結果が入っています。

  • selenium2 webdriver対応(これは0.9.1.xで対応済みでしたが)
  • spec(単体テスト)でコードカバレッジを出力できるようにした
  • story(Behat)のステップ翻訳ファイルを置き換えられるようにした
  • プラグイン内のコードインデントを4tabに統一

0.9.2よりちょっと前の改修について

v0.9.1においては、Authコンポーネントを利用した場合にうまく動作しないというポレートを海外から受けました。

これはAuthの問題というよりも、Authでログインした結果リダイレクトが動くので、リダイレクト先が正しいか検証したい、ということでした。

これはBehat&Mink側の問題だけど、使ってくれているのでMLとか漁ったところ、goutte&guzzleではパラメータを指定しろ、という変更が入っていることがわかり、behat.yml.defaultに以下の設定を追加しました。

      goutte:
        guzzle_parameters:
          request.params:
            redirect.disable: true

その上で、サンプルアプリにAuthのケースを追加し、動作検証後にリリース。質問者に返信を打ってIssueをCloseしました。

リダイレクトURLを検証する方法としては、以下のサンプルシナリオのようになります。

  シナリオ: 記事を追加できること
    前提 "トップページ" を表示している
    もし "追加" のリンク先へ移動する
    かつ "bob"、"obo"でログインする
    かつ I do not follow redirects
    かつ 記事投稿フォームに以下の内容を登録すること:
      | ラベル  | 値                |
      | タイトル | 今日は歓送迎会      |
      | 本文    | 19:30からせかいいち |
    かつ "投稿" ボタンをクリックする
    もし I should be redirected to "/posts"
    ならば "今日は歓送迎会" と表示されていること
  • I do not follow redirects と宣言することで、リダイレクトがHTTPヘッダに入っていても自動ではリダクレクとしなくなります(redirect.disable: trueが有効になる)。
  • 「"投稿“ ボタンをクリックする」の後で一覧画面にリダイレクトされるので、I should be redirected to ”/posts" のように指定したURLにリダイレクトしようとしているか検証します。検証結果が正しければ、そのURLに遷移してテストが継続します。

つまり自動リダイレクトをOFFにして、検証が成功したら遷移して継続という流れです。
日本語のステップにはなっていませんが、必要であれば独自のステップ定義を作っていただき、そこから英語ステップを呼び出すという方式でもできると思います。ただ、ここまでテクニカルというか内部動作をシナリオに書くのはユーザーストーリーとしては微妙だと思うので、ステップ定義内で使えるステップぐらいの認識で利用していただければと思います。サンプルアプリのストーリーは、問い合わせのあった形式に合わせているので、そこはあまり意識されていません。

0.9.2ハイライト

コードカバレッジ

みんな大好きJenkinsなどでCI環境を作ってコードカバレッジなどを計測している現場では、PHPUnitではカバレッジレポートが出せるのに、BddPluginでは出ないのはアレだよね、ということで対応しました。

lib/Cake/Console/cake Bdd.spec --coverage-html report

でコードカバレッジを出力できるようになっています。

  • 出力できるレポートはPHPUnitがサポートしている coverage-html, coverage-clover, coverage-php, coverage-textの4つです。
  • カバレッジ対象のディレクトリは app 以下です(ただしapp/Pluginおよびapp/Vendor, app/Testは対象外です)。

Bdd.story側はseleniumとの兼ね合いがあって単純ではないので、shin1x1さんの「コードカバレッジ測定ツールPHP_CodeCoverageをCakePHPで使ってみた」を参照にすると良いかなと思います。細かいコードの利用方法などはPHPUnitのバージョンによって微妙に異なるのでPHPUnitの公式ドキュメントを一度確認した方が良いと思います。

CakePHP2.3からはPHP5.4のサーバ機能が動くのでそれを絡めるとカバレッジレポートが取りやすくなるのかな?などと思ったりしておりますが。。。

mink-extensionのi18nステップファイルの置き換えを可能に

Behat&MinkをBddPluginから利用する場合、シナリオに「前提 “トップページ” を表示している」のように書けたりするわけですが、これらはすべてmink-extensionのi18n機構で翻訳されています。例えばこのステップは

        <trans-unit id="i-go-to-page">
            <source><![CDATA[/^(?:|I )go to "(?P<page>[^"]+)"$/]]></source>
            <target><![CDATA[/^(?:|ユーザーが )"(?P<page>[^s]+)" へ移動する$/]]></target>
        </trans-unit>

というxmlで変換定義されているのですが、見て分かる通り正規表現です。PHPで正規表現、それに日本語というと、まぁ考慮しないといけませんね。pregmatch にUTF-8オプションを付けないと「s」が変なところでマッチして期待どおりに動きません。@kaz29さんから指摘を受けたのは「"管理者画面“へ移動する」みたいに「者」が入っているとステップが認識されないというものでした。

現時点 mink-extension へ Pull Request しているので、そのうち新しいmink-extensionのバージョンで有効になると思います。

Mink-ExtensionはComposerで依存関係を解決しているので、そこのバージョンが新しくなるまでしばらく時間がかかると思います。

それまでの対策(もしくは別の言語ファイルを入れたい場合など)として、/features/steps の下に i18n ディレクトリを作成することで、そのディレクトリを翻訳ファイルの置き場にすることができます。

上記の問題で対応版のxliffを入れたい場合、必要なファイル(例えば ja.xliff )を features/steps/i18n/ja.xliff といったパスになるように入れておいてください。これは差分インストールではなく置き換えになりますので、必要な言語ファイルはすべて配置する必要があります。

さいごに

このように、地道に修正も行っていますので、たまにGithubのリポジトリを訪れてもらえればと思います。

もしBddPluginを使ってみたいけど、よくわからない!!という場合は、twitterなどで@sizuhikoにメンションもらえればと思います。時間を調整してお会いしたり、もくもく会や勉強会のようなイベントを企画したりします。

うまく動かない!!というケースは、Githubのissueに書いてもらえると助かります。もちろん日本語でOKです。

これBehatやSpec4PHPの問題っぽいよね、と思ってもOKです。どしどしご意見&ご感想をください。

ボクは連携部分を作っただけですが、BddPluginのまとまりとしては中々良いものだと思いますので(手前味噌ですが)、少しでも多くの方にご体験いただければと思っています。

PHP Matsuri2012を終えて 〜 Retrospective of PHP Matsuri 2012 in Fukuoka

<!– more –>
On this year, PHP Matsuri had convened of 3rd time around.
The participant who first timer and the repeater are split 50:50.
It become common composition of the participant in the event of 100 person scale.

As a motive of participation are followings:
1. Repeater brings the friend of the first participation.
2. Participant in the vicinity who saw last year’s article
3. Enthusiastic fan

I think that PHP Matsuri has become a annual event at late autumn.

As the PHP Matsuri staff, this maybe first post. So I stop it only in an easy summary.
To be continues and the details are posted by DANCHO.

PHP Matsuriも3回目の開催となりました。
参加者は、始めての人が半分ぐらい、リピーターが半分ぐらいで、100人規模のイベントでは良くある人数構成になってきました。
参加の動機としては、
1. リピーターの人が初参加の友人を連れてくる
2. 去年の記事を見た、近隣の参加者
3. 熱狂的ファン
があると思っています。

そういう意味でPHP Matsuriは晩秋の恒例行事となってきたのではないかと思います。

PHP Matsuri 青年団の最初をきって書くので、簡単なまとめだけに留めたいと思います。詳しくは団長が…..

Thanks:


It had been offered by many sponsors from the enterprise to the individual.
As a result, a very gorgeous prize was able to be prepared. And, it became a very happy event.
I’m really thankful.

感謝:
今年は、企業から個人まで多くのスポンサー様にご提供いただき、大変豪華な賞品を用意でき、とても楽しいイベントになりました。本当に感謝しております。

This year’s challenge:

The 1st time and the 2nd times were the same hotel groups.
It is easy to have done arrangements and the preparation about the hall.
However, this year of the third times was a hall of “Fukuoka Prefecture Ruby and contents industry promotion center” that a public organization manages .
(The hotel prepared it until last year. )Because the task had increased, the staff became busy.
However, the registration fee became low price, and the prize of LT became gorgeous.

今年のチャレンジ:
1回目、2回目とも同じ系列のホテルだったこともあり、会場に関する手配や準備はとてもやりやすかったです。3回目の今年は「福岡県Ruby・コンテンツ産業振興センター」という公的な機関が運営する会場でした。ホテルの人が担当してくれていた雑務をスタッフ主導でやることは大変でしたが、その分参加費が安くなったりLTの賞品が豪華になったことで参加者へ還元できたのではないかと思っています。

This is good:


It is some the improved points that it is a few problem until last year
1. The wifi doesn’t disconnect. We think that we solved the problem of the network because we have controlled the connection with 3 access point.
2. Just good temperature.
There were some comments that it was cold, too. However, I think the improvement to have been done since last year (Because the hall is too wide and air-conditioning was not effective).
3. Neither the conference nor the workshop get entangled. it became a concentrating workshop by using another conference room.

特に良かった事:
昨年までちょっと課題だと思っていた部分で、いくつか改善された点ですが
1. 無線が切れない。うまく分散することで、ネットワークの問題はなかったと思います
2. ちょうど良い温度。若干寒いというご意見もありましたが、昨年は会場が広すぎて空調が効かなかったりしたので、改善はされていたと思います
3. 講演とワークショップが混線しない。会議室が別だったこともあり、ワークショップを集中して開催できたと思います

Important things :


These are important thing in the event. I think that I was able to do this this year.
1. We are nice smile
2. It doesn’t become loneliness. If first timer that there was not a friend either, then I think that they were able to exchange it at the ice break and break-time.
3. Meal is delicious.
4. Wanna become repeater. I think this to be feelings that each one was able to take home.

イベントで大事な事:
私がイベント大事だと思っていることで、今年も実践できていたと思います
1. みんな笑顔
2. ボッチを作らない。もちろん始めての参加で友人もいなかったという人もいると思いますが、アイスブレークや食事時間に交流できたと思います
3. 食事が美味しい
4. また参加したくなる。これは参加者皆さんが持ち帰っていただけた気持ちと期待しています

At last, My presentation:


In this year, I had plan to make demonstrate application look like the micro framework with using followings component:

The demonstration was not complete.
When the sample application program became runnable a little more well, I want to announce in Blog or the PHP study meet up.

最後に私事:
今年は以下のどちらかを使って、マイクロフレームワーク的なコードが書けるところをデモする予定でした。

で、結局 Respectのバグっぽい箇所をふんでしまって、朝方6時ぐらいに Rails Routerに切り替えたけど、こっちはこっちでうまく動作しなくて、8時ぐらいに元鞘に。うまく動いている箇所だけで発表しました。
これらは、もうちょっとうまく動くサンプルアプリが作れたら、BlogやPHP勉強会などで発表したいと思います。

See you PHP Matsuri 2013 (maybe HOKKAIDO) !!

また来年のPHP Matsuri(たぶん北海道)で会いましょう