Technote

by sizuhiko

CakePHPのデータジェネレータFabricateにsequenceなどの機能を追加しました

<!– more –>先日カッとなって作った Fabricate ですが、その後実運用していくうちにいくつかの機能でRuby本家に近づけたくなったので実装を追加しました。

GitHub: sizuhiko/Fabricate

変更概要

  • closure だけでなく、array 形式での属性値変更が可能に
  • 初期連番をconfigで指定可能に
  • sequenceメソッドで柔軟な採番が可能に

array形式での属性値変更

これまで属性値を自動生成でなく、自分で決めた値にしたい場合は、無名関数(closure)でarrayを戻す必要がありました。
これを直接引数でarrayを指定できるようにしました。

従来の記述方法:

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

新しくサポートした記述方法:

Fabricate::create('Post', 10, ["created" => "2013-10-09 12:40:28", "updated" => "2013-10-09 12:40:28"]);
});

もちろん、第二引数の生成レコード数を省略して1レコードだけ作ることも可能です。

$dataの内容見ないのに、とか全部固定なので無名関数じゃなくても良い等のシーンが思いのほか多いもので。

初期連番をconfigで指定

これまで連番(スキーマで数値カラムになっている項目は自動的に連番対象になる)は1から常に始まるようにしていたのですが、こちらもやはり変更したい場合があって、本家同様にconfigで変更できるようにしました。

Fabricate::config(function($config) {
    $config->sequence_start = 100;
});

config関数は、環境定義オブジェクトのインスタンスを引数に取るメソッドを呼び出すので、その中で public 属性値である sequence_start の値を変更してください。
config関数を使わない場合は、従来通り1から開始されます。

sequenceメソッドで柔軟な採番

これが今回のバージョンアップで最も対応したかった機能で、連番を項目毎に変更したり、独自フォーマットに変更できたりします。
この機能に対応するために、attributes_for(), build(), create() の各メソッドで属性値を上書きできる無名関数に第二引数を追加しました。

Fabricate::attributes_for('Post', 10, function($data, $world){
    return [
        'id'=> $world->sequence('id'),
        'title'=> $world->sequence('title', 1, function($i){ return "Title {$i}"; })
    ];
});

上記のように、$world を引数に取るようになりました。もちろん利用しない場合は省略可能です。
$worldは新しく追加したクラス FabricateContext のインスタンスで sequence メソッドを持っています。

sequenceメソッドは、3つの利用方法に対応しています。

  • configで設定された開始値(または1)から開始する連番にする:

    $world->sequence(‘id’)

  • 開始番号を指定して連番にする:

    $world->sequence('id’, 10) // この例では10から開始する

  • 独自形式のユニーク文字列を作成する:

    $world->sequence('title’, function($i){ return “Title {$i}”; } // または開始番号を指定して $world->sequence('title’, 1, function($i){ return “Title {$i}”; }

一番上の利用方法は、DBのスキーマ定義が数値である限りは、sequenceメソッドを使わなくても同じ連番となるので、あまり利用ケースはないと推測されます。

本家のFabricator では同一プロセス中でのSequenceは常にユニークな値になるようになっていますが、CakePHPプラグイン版ではデータ生成のattributes_for(), build(), create() 各メソッドの呼び出し単位でユニークになります。
つまり呼び出し毎に開始番号から採番されます。

もし要望があれば、プロセス単位に連番を管理することも可能なので、Githubのissueなどで連絡ください(日本語でok)。
おそらく対応としては config で定義でできるようにすると思います…(併用でなく、プロセスか生成毎かを選択するイメージ)

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のまとまりとしては中々良いものだと思いますので(手前味噌ですが)、少しでも多くの方にご体験いただければと思っています。