Caution: You are browsing the legacy symfony 1.x part of this website.

アプリケーションをテストするには?

1.2
Language

symfonyフレームワークは常に機能テストフレームワークを搭載してきており、これは主要な強みの1つです。

機能テスト(functional test)とは何でしょうか?機能テストのゴールはすべてのアプリケーションレイヤーの統合: ルーティングからコントローラ、テンプレート、とデータベースのコールをテストすることです。 これらはユニットテストの置き換えにはなりません。

唯一簡単にテストできないものはテンプレートに埋め込まれたJavaScriptのコードです。 もちろんこのためにseleniumのようなツールを利用できます。 しかし良い知らせは機能テストはAjaxのような"何かの"JavaScriptコードをテストできることです。

仕事を行うために、機能テストフレームワークはブラウザをシミュレートします。 symfonyの内部とリクエストに基づいてレスポンスを生成する方法を知っているのでこれはWebサーバーを必要としません。 これによってそれぞれのリクエストの後でアプリケーションの状態のイントロスペクションを簡単で深く行うことができます。 もちろんレスポンスもしくはユーザーセッションのようなsymfonyコアオブジェクトだけでなく、モデルのようなあなた独自のコードもイントロスペクトできます。

symfonyがリリースされるごとに機能テストフレームワークはより改善されます。 今日は、symfony 1.2に追加されたすべてのよいところをお見せします。 驚く用意をしてください!

デカップリング

みなさんがご存じの通り、私はテストがとても好きです。 古いコードをよりベターなものにリファクタリングすることも好きです。 symfony 1.2に関しては、ブラウザ(sfBrowser)とテストブラウザ(sfTestBrowser)クラスをはるかに柔軟で設定しやすくするためにリファクタリングしました。

symfony 1.2に関しては、機能テストフレームワークは再利用可能で異なるいくつかのレイヤーで構成されています。

最も大きな変更はテスターの導入です。 テスター(tester)はアプリケーションの特定のレイヤーをテストする方法を知っているオブジェクトです。 リクエスト、レスポンス、ユーザー、ビューキャッシュ、フォームとPropelに関してsymfonyはいくつかの組み込みのテスターを備えています。

重要度の低い変更はsfTestFunctionalクラスの導入です。 このクラスはアプリケーションをテストしてすべての登録されたテスターを管理するためにsfBrowserオブジェクトに依存します。

次は典型的な機能テストです:

$browser = new sfTestFunctional(new sfBrowser());
 
$browser->
  get('/')->
  // テストを行う
;

symfony 1.0と1.1の後方互換性を維持するには、新しく非推奨のsfTestBrowserクラスをまだ利用できます:

$browser = new sfTestBrowser();
 
$browser->
  get('/')->
  // 何かテストを行う
;

テスター

ですので、実際にはすべてのテストはテスタークラスで行われます。 テスターはアプリケーションの特定の部分をテストする方法を知っています。

テスターはcheckResponseElement()もしくはisRequestParameter()のような、あなたが慣れ親しんだすべてのメソッドを置き換えます。 もちろん、後方互換性を維持するためにこれらのメソッドはまだ利用できます (UPGRADE_TO_1_2 ファイルにはすべての古いメソッドとテスターの対応するメソッドの対照表があります)。

次のコードはリクエストテスターを利用することでisRequestParameter()呼び出しを置き換える方法を示すシンプルな例です:

// symfony 1.2以前
$browser->
  get('/')->
 
  isRequestParameter('module', 'foo')->
  checkResponseElement('h1', 'foo')
;
 
// symfony 1.2の場合
$browser->
  get('/')->
 
  with('request')->isParameter('module', 'foo')->
  checkResponseElement('h1', 'foo')
;

with('request')呼び出しはすぐ次の呼び出しに対してリクエストテスターへの 流れるようなインターフェイスのコンテキストを切り替えます。 ですので、isParameter()メソッドはsfTesterRequestメソッドです。

コンテキストはテスターオブジェクトである呼び出しのブロックを作ることもできます:

$browser->
  get('/')->
 
  with('request')->begin()->
    isParameter('module', 'foo')->
    isParameter('action', 'index')->
  end()->
 
  checkResponseElement('h1', 'foo')
;

begin()end()の間のすべてのメソッド呼び出しは現在のテスターオブジェクトに対して呼び出されます。

組み込みのテスタークラスによって提供されるテストメソッドを見てみましょう。

リクエストテスター

requestテスターはsfTesterRequestクラスで定義され次のメソッドを持ちます:

メソッド 説明
isParameter リクエストパラメーターをテストする
isMethod リクエストメソッドをテストする
isFormat リクエストフォーマットをテストする
hasCookie リクエストが渡されたCookieを持つかテストする
isCookie Cookieの値をテストする
$browser->
  get('/')->
 
  with('request')->begin()->
    isParameter('module', 'foo')->
    isMethod('get')->
    isFormat('html')->
    hasCookie('foo')->
    isCookie('foo', 'bar')->
  end()
;

レスポンステスター

responseテスターはsfTesterResponseクラスで定義されており次のメソッドがあります:

メソッド 説明
isStatusCode レスポンスステータスコードをテストする
contains 単純な正規表現でレスポンスの内容をテストする
isHeader 与えられたヘッダーの値をテストする
checkElement CSS3セレクターの値をチェックする
$browser->
  get('/')->
 
  with('response')->begin()->
    isStatusCode(200)->
    contains('foo')->
    isHeader('Content-Type', 'text/plain')->
    checkElement('ul.foo li:last', '/foo/')->
  end()
;

ビューキャッシュテスター

view cacheテスターはsfTesterViewCacheクラスで定義され次のメソッドを持ちます:

メソッド 説明
isCached ページ/アクションがキャッシュの中にあるかチェックする
isUriCached 特定のURI(パーシャルも可能)がキャッシュの中にあるかチェックする
$browser->
  get('/')->
 
  with('view_cache')->begin()->
    isCached(true)->
    isUriCached('@sf_cache_partial?module=foo&action=_partial&sf_cache_key=some_cache_key')->
  end()
;

ユーザーテスター

userテスターはsfTesterUserクラスで定義され次のメソッドを持ちます:

メソッド 説明
isCulture ユーザーのcultureをテストする
isAuthenticated ユーザーが認証されたかチェックする
hasCredential ユーザーのクレデンシャルをチェックする
isAttribute 与えられた属性の値をテストする
isFlash flash変数の値をテストする
$browser->
  get('/')->
 
  with('user')->begin()->
    isCulture('fr')->
    isAuthenticated(true)->
    hasCredential('admin')->
    isAttribute('sfguard_user_id', '3')->
    isFlash('notice', '/foo/')->
  end()
;

フォームテスター

新しくセクシーなテスターを発見する時間です!

formテスターはsfTesterFormクラスで定義されています。 これはフォームが以前のリクエストで使われたかどうか知っており、フォームオブジェクト自身への参照を持ち、イントロスペクトが許可されているのか知っています。

メソッド 説明
hasErrors 投稿されたフォームがエラーを持つかチェックする
isError 与えられたフィールドに対するエラーの値をテストする
hasGlobalError グローバルエラー以外はisErrorと同じ

isError()メソッドはcheckResponseElement()メソッドと同じ種類の2番目の引数を取ります。

$browser->
  click('save', array(...))->
  with('form')->begin()->
    hasErrors()->
    hasGlobalError('The login and password does not match.')->
    isError('name', 'Required.')->
    isError('name', '/Required/')->
    isError('name', '!/Invalid/')->
    isError('name')->
    isError('name', false)->
    isError('name', 1)->
  end()
;

Propelテスター

propelテスターは別の偉大なテスターです。

HTMLレスポンスチェックを置き換えませんが、テストすることが重要であるにもかかわらずブラウザに表示されないものをチェックするための手段です(たとえばユーザー用のlast_connectionタイムスタンプが更新されてきたかどうか、もしくは記事用のビューの番号が増分されてきたかどうか)。

propelテスターはPropelプラグインのsfTesterPropelで定義され使う前に登録しなければなりません:

$browser->setTester('propel', 'sfTesterPropel');

テスターが登録された後で、これをテストの中で利用できます:

$browser->
  post('/')->
  with('propel')->begin()->
    check('Article', array('title' => 'foo'), false)->
    check('Article', array('title' => '!foo'), false)->
    check('Article', array(), 4)->
    check('Article', array('title' => '%foo%'), true)->
    check('Article', array('title' => '!%foo%'))->
    check('Article', $criteria)->
  end()
;

propelテスターは1つのメソッド: check()だけしか提供しません。 このメソッドはあなたが渡した引数に基づいて異なるふるまいをします:

  • 最初の引数はモデルクラス名
  • 2番目の引数はCriteriaオブジェクトもしくは条件のシンプルな配列
  • 3番目の引数は次の通り:
    • オブジェクトが条件にマッチする場合はtrue
    • オブジェクトが条件にマッチしない場合はfalse
    • マッチするオブジェクトの数をチェックするために整数

テスターを拡張もしくは作成する

テスターにはいくつかの利点があります:

  • 分離: テスターのデカップリングのおかげで、以前よりもはるかに多くのテストメソッドを提供できます。
  • 可読性: ブロックの概念と短いメソッドの名前のおかげで、テストはいっそう読みやすいです。
  • 拡張性: それぞれのテスターをあなた独自のメソッドで拡張できるもしくは独自のテスタークラスを作成できます。

組み込みのテスターを拡張する

既存のテスターにメソッドを追加したい場合、組み込みのテスターを継承するクラスを作り独自クラスの名前で再登録する必要があります:

class ApplicationTesterRequest extends sfTesterRequest
{
  // テスターメソッドを追加する
}
 
// 機能テストの中で
$browser->setTester('request', 'ApplicationTesterRequest');

組み込みテスターのまとまりをオーバーライドする必要がある場合、setTestersメソッドが使えます:

$browser->setTesters(array(
  'request'  => 'ApplicationTesterRequest',
  'response' => 'ApplicationTesterResponse',
));

テスターメソッドは望むことは何でもできますが流れるインターフェイスを正しく動作させるためには常に次のコードで終わらなければなりません:

return $this->getObjectToReturn();

メソッドの中では、いくつかのオブジェクトにアクセスする権限があります:

  • $this->browser: 現在のブラウザーメソッド
  • $this->tester: lime_testオブジェクト

新しいテスターを作成する

ユニークな名前で登録することで新しいテスタークラスも登録できます:

$browser->setTester('my_tester', 'myTester');

テスタークラスはsfTesterを継承し次のメソッドを実装しなければなりません:

  • initialize(): テストでwith()を使うたびにこのメソッドが呼び出されます。 これはリクエストが送信された後でオブジェクトを取得するのに便利です:

    public function initialize()
    {
      $this->request = $this->browser->getRequest();
    }
  • prepare(): ブラウザオブジェクトへのコールの直前にこのメソッドが呼び出されます。 これはリクエストが送信される直前に何かを行う必要がある場合に便利です。

流れるようなインターフェイスにする

与えられたモジュールに対してたくさんの機能テストを書くとき、何が行われているのか視覚的な情報があると便利であることがあります。 新しいテスターは新しいレベルのインデントを追加するのでテストは読みやすくなります。

また、テストの分類を手助けするためにテキストを出力する新しいinfo()メソッドがあります:

$browser->
  info('First scenario: Form with errors')->
  // ... 何かのテスト
  info('Second scenario: Valid form submission')->
  // ... さらに何かのテスト
;

ブラウザーの情報

テストをデバッグする

機能テストで問題が起きるとき、ブラウザーに転送されるHTMLは原因の診断の手助けをしてくれます。 symfony 1.2に関しては、流れるようなインターフェイスのスタイルを乱すことなく、生成されたHTMLを表示するのはとても簡単です:

$browser->
  get('/a_uri_with_an_error')->
  with('response')->debug()->
  // 実行されないテスト
;

debug()メソッドはレスポンスヘッダーと内容を出力しブラウザーの流れを中断します。

同じdebug()メソッドがformテスト用にも存在し投稿された値と存在するのであればフォームエラーを出力します:

$browser->
  post('/post_to_a_form_with_some_errors')->
  with('form')->debug()->
  // 実行されないテスト
;

デバッグ

今日はこれでお終いです。symfony製のアプリケーションをテストすることが簡単になったわけではありません。 ですので、新しいテストフレームワークが難しくなく時間を節約することをあなたが理解してくださることを期待しております。

新しいWebデバッグツールバーのパネルに関しては、新しいテスターを作成するのであれば、これらをプラグインパッケージとして作成することをためらわないでください。