Lithiumのユニットテスティングフレームワーク


今日は、国際化をとばしてテストを読みました。
テストというのはかなり重要で個人的な経験ですが書かなくてえらい苦労しました。CakePHPでもテストはもちろん書けますが個人的にsimpletestは(あまり保守されていませんし)好きじゃないのでこのテスティングフレームワークには期待しています。
最近の状況ですとPHPUnitを使うことが一番良い(他の多くのフレームワークPHPUnitに移行していますし継続的インテグレーション等を考慮すると)とは思いますが、Lithiumの開発者はsimpletestの経験やCakePHPでの開発経験を活かしたうえで新たなテスティングフレームワークを構築しているはずですので、そういったトレンドは無視してLithiumのソリューションに乗っかってみるのも良いと思います。
僕の予想ですが、「テストも使いやすくシンプルでRad」というフレームワークの思想と既存のものとが合わなかったのかもしれません。

まず、テストを気軽に書いて試してみたいというのであれば、下記の動画が役に立ちます。たいしたことはやっていませんがとっかかりにこういうのは必要ですね。公開されている方に感謝です。
Test Driven Development in Lithium

以下からが公式のドキュメントになります。

参照元10_testing/testing.wiki

多くの複雑性と再利用性を伴ったアプリケーションにはテストカバレッジが必要になります。Lithiumのユニットテスティングフレームワークは内成化されており、フレームワーク自体のテストにも使われています。それは簡単でライトウェイトですぐに使えます。

はじめよう

テスティングフレームワークLithiumにビルトインされていますので、既に確認されているかもしれません。LithiumをダウンロードしインストールしればアプリケーションのベースURL下の/testをブラウザでアクセスして見てください。
Lithiumユニットテストダッシュボードはテストケースを閲覧でき、テストを実行したりレポートを閲覧できる場所になります。最初にLithiumコアテストを見ることもできます。
自分のアプリケーションのユニットテストをセットアップを管理してみましょう。
アプリケーションのユニットテストは/app/testsに格納されます。cases/integration, mocksという3つの主要テストフォルあがあります。casesフォルダはクラス単位のユニットテストを保持します。integrationは2つ以上のクラスをに渡るテストケースを保持します。そしてmocksはテスト中に使われる偽物のデータを作成する為に使われます。

テストケース

casesフォルダはユニットテストのコアロジック全てを格納する為に使われます。もし/app/tests/cases内を見たことがあるのでしたら、既に3つのフォルダがアプリケーションのユニットテストを編成するために使われていることに気づいたことでしょう。このフォルダ構造はそれぞれのユニットテストケース毎に名前が決まっています。通常アプリケーションのクラス名/名前空間の構造と同じになっているはずです。
動作しているサンプルとして簡単なテストを作成することから始めます。最初の例はモデルのユニットテストになります。li3 createコンソールコマンドを使って作成します。

$ cd /path/to/lithium/app
$ li3 create model Post

Post created in app\models.

テストケースを作成する為にli3 createコマンドを使うこともできます。

$ li3 create test model Post

PostTest created for Post in app\tests\cases\models.

\lithium\test\Unitクラスを継承したテストファイルのテンプレートクラスを作成すると次のようなコードができます。

<?php

namespace app\tests\cases\models;

use app\models\Post;

class PostTest extends \lithium\test\Unit {

    public function setUp() {}

    public function tearDown() {}

}

?>

名前どうりの動作をする2つの初期メソッドが提供されます。setUp()メソッドはユニットテストロジックを実行するのに必要な前処理を実行するのに使われます。これはデータベースのコネクションの設定からモックデータを初期化といったことに使えます。同様にtearDown()メソッドはユニットテストの実行が終わったときに何かをクリーンアップする為に使われますl.これらのメソッドはユニットテストケースのメソッドの前後で呼び出されます。
しかし、ユニットテストの内容は作成したメソッド内に書かれます。ユニットテストロジック毎にtestというプレフィックすが付いたメソッドの内部に配置されるはずです。Postモデルに適用する前に、少しTDDの説明と最初にテストメソッドの例を書いてみます。

テストケースはlithium\test\Unitのサブクラスとなりますので、多くのテストアサーションをバリデートするのに役立つメソッドに簡単アクセスできます。それらは適切に命名付けられているのでここでは一覧にするだけいします。より詳しい情報はlithium\test\UnitのAPIドキュメントを参照してください。

  • assertEqual()
  • assertNotEqual()
  • assertIdentical()
  • assertTrue()
  • assertFalse()
  • assertNull()
  • assertNoPattern()
  • assertPattern()
  • assertTags()
  • assertCookie()
  • expectException()

投稿はちゃんとしたタイトルが付けられ、筆者はトップテンというフレーズを含んでいる投稿のタイトルが本当に馬鹿げたことだということは分かっています。このフレーズを検索して我々に返すメソッドが最終的には必要になります。そのメソッドを書く前に、それをカバーするテストケースを作成します。testIsGoodTitles()というメソッドにします。下記がその実装になります。

<?php

namespace app\tests\cases\models;

use app\models\Post;

class PostTest extends \lithium\test\Unit {

    public function setUp() {}

    public function tearDown() {}

    public function testIsGoodTitle() {
        $this->assertTrue(Post::isGoodTitle("How to Win Friends and Influence People"));
        $this->assertFalse(Post::isGoodTitle("The Top 10 Best Top Ten Lists"));
    }
}

?>

ブラウザに戻ってユニットテストダッシュボードをリフレッシュしてみましょう。左側のリスト上にPostTestユニットテストケースである新しいエンティティがあることが分かるはずです。PostTestテストケースをクリックするとテスト結果が表示されるはずです。この時点ではモデルはまだ作成していませんので、コネクションや関数が無いとエラーを出力しているはずです。

テストをパスさせる為にモデルを作業ます。まず、モデルにコネクションを指定します。このあとで調整しますが、とりあえずシンプルにしたいのでこのようにします。

<?php

namespace app\models;

class Post extends \lithium\data\Model {
    protected $_meta = array('connection' => false);
    public $validates = array();
}

?>

これでテストをもう一度実行してもisGoodTitle()メソッドは定義されていないとエラーはでなくなります。それを満たす為にモデルに基本機能を提供しましょう。

<?php

namespace app\models;

class Post extends \lithium\data\Model {
    protected $_meta = array('connection' => false);
    public $validates = array();

    public static function isGoodTitle($title) {
        return !stristr($title, 'top ten');
    }
}

?>

この時点で、テストケースはユニットテストダッシュボードで成功しているはずです。

モック

モックは実際の情報の代わりに使われます。概ね、データソース、モデルデータ、コンソールコマンドのレスポンス等にモックを作成することができます。この例題ではモデルを主に扱っていますので、引き続き新しくモデルに追加した機能に対するテストに役立つモックを使ってみます。

それではisGoodTitle()メソッドを通して使うことが出来るテストデータを返すMockPostクラスを作成します。簡単なのは、RecordSet(SQLデータベースの場合)かDocumentのコレクション(ドキュメントデータベースの場合)を返すクラスを新規で作成することです。
app/tests/mocks/data/MockPost.phpに新しくファイルを作成します。

<?php

namespace app\tests\mocks\data;

use lithium\data\collection\RecordSet;

class MockPost extends \app\models\Post {
    public static function find($type = 'all', array $options = array()) {
        switch ($type) {
            case 'first':
                return new RecordSet(array('items' => array(
                    'id' => 1, 'title' => 'Top ten reasons why this is a bad title.'
                )));
            break;
            case 'all':
            default :
                return new RecordSet(array('items' => array(
                    array('id' => 1, 'title' => 'Top ten reasons why this is a bad title.'),
                    array('id' => 2, 'title' => 'Sensationalist Over-dramatization!'),
                    array('id' => 3, 'title' => 'Heavy Editorializing!'),
                )));
            break;
        }
    }
}

ここで取得しているものは本質的にはfind()メソッドを呼び出した時にハードコードされたデータを吐き出すモデルです。この場合、これは本当に必要なものであるかもしれません。次の関数を追加してテストケースにこのモックを使ってみましょう。

public function testMockTitles() {
    $results = MockPost::find('all');

    $first = $results->current();
    $this->assertFalse(MockPost::isGoodTitle($first['title']));
}

ユニットテストダッシュボードに戻ってうまく実行されているか確認して見てください。

英語の勉強

ドキュメントを全て読むまでは毎日投稿しようと思っていましたが、しばらくBlogの投稿をおやすみしてしまいました。仕事が忙しかったことと、だんだん自分の英語の読解力に自信が無くなってきたことが原因です。

いちおう英語は(あまり熱心ではありませんでしたし成績は悪かったですが) 中学で3年、高校で3年勉強しましたので、英語で話したり、リスニング等はできなくても英語で書かれた文章が自分の興味のある話題であればなんとかなると思っていましたが、やはりやり続けると限界を感じてきました。

個人的に「英語は勉強する必要は無い!」とずっと思ってました。
「向こうで暮らしたり、外人と付き合えば普通に分かるようになる(それが自然)」、「勉強しないから分からないのではなく活用する機会が無いから英語って分からないだけ」と思っていたのですが普通に日本に住みながら英語の情報を収集する必要がある状況ではそう都合がよく楽な方法はなく「本物の英語ではなくてもいいから自分にとって必要のある英文の情報を理解する"テクニック"」を取得する為に勉強することは必要なことだと思い直しました。

いろいろ調べた結果、そういった僕のニーズを満たすハウツー本が下記の本(著者)でして少しずつ勉強をしております。勉強が終わったら今のなんちゃって訳ではなくちゃんとした訳に直そうかとも思ってます。
文法って必要ないって思ってましたが結構大事なんですね。これらの本(著者)はAmazonでも高評価である一方で批判もありますが、結局、学習する方の目的が違うのだと思います。「ネイティブな英語がしゃべれるようになりたい」という目的ではなく「必要な情報を理解する為のテクニックを身につけたい」という本来の目的を忘れてしまうとさしあたって必要のない勉強を永遠としてしまうことになります。
逆に前者の目的に該当する方はたとえばハリウッド俳優を目指しているだとか特殊な人だけだと思われますので僕はこれで十分だと思っております。

基本からわかる英語リーディング教本

基本からわかる英語リーディング教本

「本当」の基本を理解する 英語リーディングパズル

「本当」の基本を理解する 英語リーディングパズル

学校で教えてくれない英文法―英語を正しく理解するための55のヒント

学校で教えてくれない英文法―英語を正しく理解するための55のヒント

訳がわからなくて困った時に、とりあえず品詞分解してみればなんとかなる「セーフティネット」があるような感じがとても良いです。これまでの読んだドキュメントを読み直したりしながら「あー!」みたいなことも多くありました。特にLithiumのフィルターのところなんかそうですね。

こんな勉強をやり直すなんて夢にも思いませんでした。いつも時代にも誰からも思われる「あの時もっと勉強していればよかった!」というフレーズがもちろん今の僕の頭の中にもありますが、「学び」ということに関して遅いということは無いと思います。

簡単なユーザ認証


今日は、認証です。ログイン画面等を作るのに必要ですね。

参照元06_auth/simple-auth-user.wiki

MVCでユーザを作成する

まず、少なくともプライマリーキー(id)とユーザ名(username)とパスワード(password)のフィールドを持ったユーザのデータを作成します。(もちろんこれらは名前を変更することができますが、設定が必要になります)

モデル

モデルをapp/models/User.phpというファイルで作成します。

<?php

namespace app\models;

class User extends \lithium\data\Model {
}

User::applyFilter('save', function($self, $params, $chain){
    $record = $params['record'];
    if (!$document->id) {
        $document->password = \lithium\util\String::hash($document->password);
    }
    if (!empty($params['data'])) {
        $document->set($params['data']);
    }
    $params['record'] = $record;
    return $chain->next($self, $params, $chain);
});

?>

コントローラ

次にapp/controllers/UsersController.phpというファイルでコントローラを作成します。

<?php

namespace app\controllers;

use lithium\security\Auth;
use app\models\User;

class UsersController extends \lithium\action\Controller {

    public function index() {
        $users = User::all();
        return compact('users');
    }

    public function add() {
        $user = User::create($this->request->data);

        if (($this->request->data) && $user->save()) {
            return $this->redirect('Users::index');
        }
        return compact('user');
    }
}

?>

ビュー

以下の2つのビューを作成します。
app/views/users/add.html.php

<h2>Add user</h2>
<?=$this->form->create($user); ?>
<?=$this->form->field('username'); ?>
<?=$this->form->field('password', array('type' => 'password')); ?>
<?=$this->form->submit('Create me'); ?>
<?=$this->form->end(); ?>

app/views/users/index.html.php

<h2>Users</h2>
<?php foreach ($users as $user) { ?>
    <?=$user->username; ?><br />
<?php } ?>

LithiumのStringクラスに暗号化のライブラリーがあるということが分かったのと暗号化もFilterを使うんですね。少しドキュメント的には不親切ですけどコードがあればなんとなく分かるので別にいいかと思います。

モデルに機能を追加する


今日はモデルです。個人的に多くの情報が欲しいのはヘルパーとモデルなんですが、ドキュメントではそれらの内容が充実していません(下記の内容だけでモデルは終わってしまいます)。
個人的にMongoDBの知識がないのでドキュメントデータベースとのやりとりなどの情報が欲しいです。

参照元05_models/adding_functionality.wiki

  1. スタティックメソッド:モデルに適用します。
  2. インスタンスメソッド:それぞれのレコードに適用します。

一般的には、スタティックメソッドはデータアクセスへの準備に用意されます。一方インスタンスメソッドはデータが既にサクセスされた時に大変役に立ちます。

例題:余分なデータを準備する。

<?php
namespace app\models;

class User extends \lithium\data\Model {

    protected static $_roles = array('admin', 'friend', 'stranger');

    public static function roles() {
        return static::$_roles;
    }
}

例題:ビューで表示する為にnameメソッドを追加する

<?php
namespace app\models;

class User extends \lithium\data\Model {

    public function name($record) {
        return "{$record->firstName} {$record->lastName}";
    }
}

作成したメソッドを使う

<?php foreach ($users as $user) { ?>
    <p>Name: <?=$user->name()?></p>
<?php } ?>

短いですが有益な情報だったと個人的には思いました。ドキュメントというよりTipsとかCookbookみたいな内容だとは思いますがLithiumのモデルをうまく利用する為の基礎的なことだと思います。

ヘルパー


今日はヘルパーです。現在(2011.04.10)の時点ではヘルパーのドキュメントが崩れてソースコードがよく分からないでなんとかして欲しいですが、タイムライン等から把握するとstableリリースへ向けてバグを取ったりしているようで(mongodbのバグ報告等をよく見かけます)忙し過ぎてドキュメントに手が回ってないのかもしれません。とりあえず読むことが可能なところだけ読んでいます。

参照元03_request_response/03_helpers.wiki

Lithiumのヘルパーはエレメントの概念を一歩進め、アプリケーションに必要な再利用可能なプレゼンテーションロジックを格納する為の場所を提供します。

使い方

Lithiumのヘルパーはレンダーされるときにレイジーロードされますので簡単に使えます。ビューレイヤでヘルパーを使うためにはレンダーのプロパティとしてただ参照すれば良いだけです。

<?= $this->html->link('link', 'http://lithify.me') ?>

(※注:崩れていて読み取れませんが、上記のような感じで書かれているのだと思います)

このアプローチにより使用時にヘルパーの宣言が必要なくなりますし、それらのヘルパーを実際に使わない時はロードされません。
ビューレイヤ(レイアウト内、ビューテンプレートもしくはエレメント)のどこでもヘルパーを使うことができますので覚えておいて下さい。

自作のヘルパーを作成する。

カスタムヘルパーを作成する為には、app/extensions/helper/ディレクトリにLithiumのベースとなるHelperクラスを継承したクラスを作成します。簡単な例として、app/extensions/helper/AwesomeHtml.phpに新しいファイルを作成して特別なリンクをソートして表示するシンプルなヘルパーを作成してみましょう。

<コードがちゃんと書かれてません。ここにコードが入るはず。>

ここで重要なことは$titleと$urlの内容はヘルパーのメソッドからそのまま返されるのでエスケープされません。安全にするにはHelperクラスのescape()メソッドを使うようにして下さい。
文字列は少し汚いように感じますので、Helperクラスの_render()メソッドを利用したいと思われるかもしれません。ヘルパーの_stringsプロパティとして文字列テンプレートの配列を作成するとうまくいきます。今作成した例題のヘルパーをセキュアでもっと拡張のある実装にしてみましょう。

public function link($title, $url, $options) {
	$title = $this->escape($title);
	return $this->_render(__METHOD__, $options['type'], compact($title, $url));
}

このように設定すると、コアヘルパーと同じように新しいヘルパーを使うことができます。

You should really check out
awesomeLink->create('Lithium', 'http://lithify.me', array(
	'type' => 'super_cool'
)) ?>

コアのヘルパーを拡張(置き換え)をする

Lithiumはコアより自作クラスを優先します。コアのヘルパーの自作拡張バージョンを作成したいのでしたら、app/extensions/helpersディレクトリ内に同じファイル名で新しくクラスを簡単に作成することができます。
例えば、LithiumのHtmlヘルパークラスを上記で新しく作成したクラスとファイル名を"AwesomeHtml"から"Html"にリネームして置き換えることができます。こうすることでテンプレートに参照を記述することなくヘルパーを自動的に参照できます。
ヘルパーを継承し既存の機能を利用したい場合は、\lithium\template\helper\Htmlを拡張した新しいヘルパーを作成し既存のHtmlヘルパーのlinkメソッドを利用したリファクタリングを行うこともできます。(オーバーライドってことだと思います)

英語が難しくともコードがちゃんとしてればなんとか読み取れますが、それが出来ないため今回はちょっときつかったです。
draftsなので当然でしょうがドキュメントが思ったより整備されていなく、残りのドキュメント調べてみたところ、モデル(あまり詳しく書いてないです)、認証、国際化、テストぐらいになってます。このペースでいくと、それらの残ドキュメントは来週中には全て読み終わってしまうと思います。正直「えっ、もう終わり!全然身についてないよー」と言いたいですが、draftsなので仕方ありませんね。CakePHPのcookbookのようなボリュームの公式ドキュメントが出るのはいつになるのか分かりませんがまだまだ先のことになりそうです。

ビュー


参照元03_request_response/02_views.wiki

MVCデザインパターンの3つの柱の一つとして、ビュークラス(他のサポートしているクラスと一緒に)はリクエストもしくはコントローラから渡されたデータを受け取り、リクエストされたテンプレート/レイアウトにデータを挿入し、完全にレンダリングされたコンテンツを返す役割があります。

特に指定が無い限り、コントローラのアクションメソッドはビューを(通常HTMLとして)レンダリングします。ビュー名とビューを格納する場所は呼び出されるコントローラとアクション名に応じて規約によって決まっています。基本的なパターンはビューはテンプレート名は{アクション名}.{メディア}.phpとなり、app/views/{コントローラ名}フォルダに格納されます。以下が使用例になります。

  • UsersController::login() --> /app/views/users/login.html.php
  • NewsItemsController::viewAll() --> /app/views/news_items/view_all.html.php

ビュー変数へのアクセス

コントローラレイヤのデータはビュー変数によりテンプレートで利用できるになります。コントローラのガイドで既に説明したように、ビュー変数はset()メソッドを使うかコントローラのアクションメソッドから連想配列をreturnすることで渡せます。
これらの配列のキーはビュー変数の名前を決定します。次の数行のコードは全てコントローラのアクションメソッドに$fooという名前のビュー変数に'bar'という内容を代入しています。

$this->set(array('foo' => 'bar'));

// -- or --

$foo = 'bar';
$this->set(compact('foo'));

// -- or --

return array('foo' => 'bar');

// -- or --

$foo = 'bar';
return compact('foo');

コントローラで上記を行えば、次のようにしてデータにアクセスできます。

<p>Spit out date like this: <?=$foo ?></p>

Lithiumのテンプレートは素のPHPですので必要な条件文、ループ、他のプレーゼンテーションベースのロジックを気軽に使用することができます。

自動エスケープ