PropelもしくはDoctrineモデルのためのユニットテストを書くのは極めて簡単です。 このチュートリアルでは、モデル用のベターなテストを書くための重要なティップスとベストプラクティスを学びます。
データベースの接続
Propelのモデルクラスをテストするには、データベースが必要です。 開発用のものが既にありますが、テスト専用のものを常に作っておくことは良い習慣です。
すべてのテストはtest
環境下で実行されるので、config/databases.yml
設定ファイルを編集してtest
環境用のデフォルト設定を上書きする必要があるだけです:
test: propel: param: dsn: mysql:dbname=myproject_test;host=localhost dev: # dev configuration all: propel: class: sfPropelDatabase param: dsn: mysql:dbname=myproject;host=localhost username: someuser password: somepa$$word encoding: utf8 persistent: true pooling: true classname: PropelPDO
このケースにおいて、データベースの名前だけを変更しましたが、データベースのエンジンを変更してたとえばSQLiteを利用することもできます。
データベースを設定したので、propel:insert-sql
タスクを使ってテーブルを作ることができます:
$ php symfony propel:insert-sql --env=test
テストデータ
テスト用の専用データベースがあるので、ユニットテストを立ち上げるたびにテストデータ(フィクスチャ)をロードする方法が必要です。 これはテストを実行するたびにデータベースを同じ状態にしたいからです。
sfData
クラスのおかげでこれを実現する方法はとても簡単です:
$loader = new sfPropelData(); $loader->loadData(sfConfig::get('sf_test_dir').'/fixtures');
loadData()
メソッドは1番目の引数としてディレクトリもしくはファイルを受け取ります。
共通のfixtures
ディレクトリは次のとおりです:
test/ fixtures/ 10_categories.yml 20_articles.yml 30_comments.yml
すべてのファイルの接頭辞として数字が追加されていることに注目してください。 これはデータをロードする順序をコントロールするための簡単な方法です。 プロジェクトで後からフィクスチャデータを差し込む必要がある場合、既存の数字の間に好きな数字を入れます:
test/ fixtures/ 10_categories.yml 15_must_be_laoded_between_categories_and_articles.yml 20_articles.yml 30_comments.yml
賢明な読者はsymfony bookではこれらのフィクスチャがdata/
ディレクトリに設置することを推奨されているのに対して、筆者がフィクスチャをtest/
ディレクトリに設置したことに注目するでしょう。
これは本当に好みの問題ですが、筆者はこれらの2つのディレクトリの中でフィクスチャを分けることを好みます。
フィクスチャを2つの異なるグループに分類できるからです:
data/fixtures
: アプリケーションを実際に稼働させるために必要なすべての初期データを含むtest/fixtures
: (単体と機能)テストによって必要なすべてのデータを含む
テストデータの一式が小さいときはこのシンプルなスキーマは立派に機能しますが、
モデルが成長し、もっとたくさんのフィクスチャを持つようになったら、データベースの中でそれらをロードする時間が重大になる可能性があります。
ですので、テストデータのサブセットだけをロードする方法が必要です。 これを行う1つの方法は主要な機能ごとにサブディレクトリを作ることで下位のテストデータの分類を行うことです:
test/ fixtures/ 10_cms/ 10_categories.yml 20_articles.yml 30_comments.yml 20_forum/ 10_threads.yml
これでメインのfixtures
ディレクトリをロードする代わりに、テストしたいモデルクラスに対応したサブディレクトリの1つだけをロードできます。
しかしたいていの場合、ユーザーのように共有データをロードすることも必要です:
test/ fixtures/ 00_common/ 10_users.yml 10_cms/ 10_categories.yml 20_articles.yml 30_comments.yml 20_forum/ 10_threads.yml
このユースケースを簡単にするために、loadData()
メソッドはディレクトリかつ/もしくはファイルの配列をとることができます:
// ユーザーとCMSのすべてのデータをロードする $loader = new sfPropelData(); $loader->loadData(array( sfConfig::get('sf_test_dir').'/fixtures/00_common/10_users.yml', sfConfig::get('sf_test_dir').'/fixtures/10_cms', ));
これによって10_users.yml
フィクスチャファイルはロードされすべてのフィクスチャファイルは10_cms
ディレクトリの中で見つかります。
ユニットテストを書く
専用データベースを既知の状態にする方法があるので、Article
モデル用のユニットテストを作りましょう。
ファイルをブートストラップするPropelの典型的なユニットテストは次のとおりです:
// test/unit/model/ArticlePeerTest.php include(dirname(__FILE__).'/../../bootstrap/unit.php'); $configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'test', true); new sfDatabaseManager($configuration); $loader = new sfPropelData(); $loader->loadData(sfConfig::get('sf_test_dir').'/fixtures'); $t = new lime_test(1, new lime_output_color()); $t->diag('::retrieveBySlug()'); $article = ArticlePeer::retrieveBySlug('the-best-framework-ever'); $t->is($article->getTitle(), 'The Best Framework Ever', '->retrieveBySlug()は特定のスラグにマッチする記事を返す');
このスクリプトの内容は一目瞭然です:
ユニットテストに関して、ブートストラップファイルを含みます。
include(dirname(__FILE__).'/../../bootstrap/unit.php');
test
環境のための設定オブジェクトを作り デバッグ機能を有効にします:$configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'test', true);
これによってPropelのクラスのオートローディングも初期化されます。
データベースマネージャを作ります。これは
databases.yml
設定ファイルをロードすることでPropelの接続を初期化します:new sfDatabaseManager($configuration);
sfPropelData
を利用することでテストデータをロードします:$loader = new sfPropelData(); $loader->loadData(sfConfig::get('sf_test_dir').'/fixtures');
すべての準備が整ったので、モデルオブジェクトのテストを始めることができます。
ユニットテストを書くことに慣れていないのであれば、最初はこわいかもしれません。
筆者がテストする必要があることを知るためにいつも使ういくつかのティップスは次のとおりです:
- 1回につき1つのクラスの1つのメソッドをテストする
- 特定の1つの入力に対して、メソッドの出力が期待するものと同じであることをテストする
- メソッドのコードを読み、存在するであろうすべてのビジネスルールをテストする
- 明らかなことや別のメソッドによって行われることはテストしない
筆者のテストファイルは同じパターンで常に構造化されます:
// テストするメソッドでメッセージを出力する(->はインスタンスメソッドに対して、::はクラスメソッドに対して) $t->diag('->methodName()'); // 1回につきシンプルなセンテンスで表現できる1つのことをテストする // センテンスは常にメソッドの名前で始まる // 動詞はしなければならないこと、どのように振る舞わなければならないことなどを表現する $t->is($object->methodName(), 1, '->methodName()は引数が渡されていない場合は1を返す');
コードカバレージ
テストを書くとき、複雑なコードの条件をテストすることは忘れがちです。
symfony 1.2はtest:coverage
というコードカバレージをテストするための小型で手軽なタスクを搭載しています。
ですので、筆者は特定のクラスに対してテストを書いた後に、すべてのテストを実行したことを確認するために常にtest:coverage
タスクを起動させます:
$ php symfony test:coverage test/unit/model/ArticleTest.php lib/model/Article.php
最初の引数はテスト用のファイルもしくはディレクトリです。 2番目の引数はコードカバレージを知りたいファイルもしくはディレクトリです。
どの行がカバーされていないのか知りたければ、--detailed
オプションを追加します:
$ php symfony test:coverage --detailed test/unit/model/ArticleTest.php lib/model/Article.php
This work is licensed under the Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License license.