Jobeetをやってみる 9日目

機能テストについて学ぶ
テスト用のデータは test/fixtures にあるものを利用するようにするけど、中身は data/fixtures と一緒。

sfBrowserクラスについて

ブラウザの動作をシミュレートするメソッド
メソッド名 説明
get($uri, $parameters, $changeStack) GETメソッドによるリクエス
post($uri, $parameters, $changeStack) POSTメソッドによるリクエス
call($uri, $method, $parameters, $changeStack) PUTとDELETEメソッドを利用したリクエス
back() 履歴の戻る
forward() 履歴の進む
reload() 更新
click($name, $arguments, $options) リンクやボタンをクリックした動作
select($name) ラジオボタンチェックボックスの選択
deselect($name) ラジオボタンチェックボックスの選択解除
restart() ブラウザの再起動
ブラウザの設定に関するメソッド
メソッド名 説明
setHttpHeader($header, $value) HTTPヘッダの追加
setAuth($username, $password) Basic認証の認証設定
setCookie($name, $value, $expire, $path, $domain, $secure, $httpOnly) Cookieの追加
removeCookie($name) Cookieの削除
clearCookies()*1 全てのCookieの削除
followRedirect() Locationヘッダによるリダイレクト
使用例
<?php
$browser = new sfBrowser();
 
$browser->
  get('/')->
  click('Design')->
  get('/category/programming?page=2')->
  get('/category/programming', array('page' => 2))->
  post('search', array('keywords' => 'php'))
;

sfTestFunctionalクラス

sfBrowserクラスを利用して、実際にテストを行なうクラス。
sfBrowserもそうだけど、sfTestFunctionalも流れるようなインターフェイスで記述できる。

categoryActionsTest.php

既にgenerate:moduleで以下のような機能テスト用ファイルが作られている。

<?php
// test/functional/frontend/categoryActionsTest.php
include(dirname(__FILE__).'/../../bootstrap/functional.php');
 
$browser = new sfTestFunctional(new sfBrowser());
 
$browser->
  get('/category/index')->
 
  with('request')->begin()->
    isParameter('module', 'category')->
    isParameter('action', 'index')->
  end()->
 
  with('response')->begin()->
    isStatusCode(200)->
    checkElement('body', '!/This is a temporary page/')->
  end()
;

begin()からend()までが所謂ブロックとなる。

しかしブロックが必須というわけではなく、1つだけの機能をテストするのであれば

<?php
$browser->
  with('request')->isParameter('module', 'category')->
  with('response')->isStatusCode(200)->
;

のように書くこともできる

Requestテスト用メソッド

sfTesterRequestクラス

メソッド名 説明
isParameter($key, $value) リクエストパラメータ値のチェック
isFormat($format) リクエスト形式のチェック(html,xml,yml,json,etc...)
isMethod($method) リクエストメソッドのチェック
hasCookie($name, $exists) 指定したCookieが存在するかどうか
isCookie($name, $value) Cookieの値のチェック
Responseテスト用メソッド

sfTesterResponseクラス

メソッド名 説明
checkElement($selector, $value, $options) CSSセレクタがCSS3の仕様に合っているかのチェック
isHeader($key, $value) ヘッダの値のチェック
isStatusCode($statusCode) ステータスコードのチェック
isRedirected($boolean = true) 現在のレスポンスがリダイレクトかどうか

テストコード

細々してどこで何をやってるかイメージしづらいし、どうせテストはその時に応じて変更されるので一気にやる
本家では loadData() を作成してテスト時にデータをロードするようにしてるけど、2回目以降でエラーになってしまうのでちょっとだけ変更

<?php
// test/functional/frontend/jobActionsTest.php
include(dirname(__FILE__).'/../../bootstrap/functional.php');
 
$browser = new JobeetTestFunctional(new sfBrowser());
 
$browser->info('1 - The homepage')->
  get('/')->
  with('request')->begin()->
    isParameter('module', 'job')->
    isParameter('action', 'index')->
  end()->
  with('response')->begin()->
    info('  1.1 - Expired jobs are not listed')->
    checkElement('.jobs td.position:contains("expired")', false)->
  end()
;
 
$max = sfConfig::get('app_max_jobs_on_homepage');
 
$browser->info('1 - The homepage')->
  info(sprintf('  1.2 - Only %s jobs are listed for a category', $max))->
  with('response')->
    checkElement('.category_programming tr', $max)
;
 
$browser->info('1 - The homepage')->
  get('/')->
  info('  1.3 - A category has a link to the category page only if too many jobs')->
  with('response')->begin()->
    checkElement('.category_design .more_jobs', false)->
    checkElement('.category_programming .more_jobs')->
  end()
;
 
$browser->info('1 - The homepage')->
  info('  1.4 - Jobs are sorted by date')->
  with('response')->begin()->
    checkElement(sprintf('.category_programming tr:first a[href*="/%d/"]', $browser->getMostRecentProgrammingJob()->getId()))->
  end()
;
 
$browser->info('2 - The job page')->
  info('  2.1 - Each job on the homepage is clickable and give detailed information')->
  click('Web Developer', array(), array('position' => 1))->
  with('request')->begin()->
    isParameter('module', 'job')->
    isParameter('action', 'show')->
    isParameter('company_slug', 'sensio-labs')->
    isParameter('location_slug', 'paris-france')->
    isParameter('position_slug', 'web-developer')->
    isParameter('id', $browser->getMostRecentProgrammingJob()->getId())->
  end()->
 
  info('  2.2 - A non-existent job forwards the user to a 404')->
  get('/job/foo-inc/milano-italy/0/painter')->
  with('response')->isStatusCode(404)->
 
  info('  2.3 - An expired job page forwards the user to a 404')->
  get(sprintf('/job/sensio-labs/paris-france/%d/web-developer', $browser->getExpiredJob()->getId()))->
  with('response')->isStatusCode(404)
;
<?php
// lib/test/JobeetTestFunctional.class.php
class JobeetTestFunctional extends sfTestFunctional
{ 
  public function getMostRecentProgrammingJob()
  {
    $q = Doctrine_Query::create()
      ->select('j.*')
      ->from('JobeetJob j')
      ->leftJoin('j.JobeetCategory c')
      ->where('c.slug = ?', 'programming');
    $q = Doctrine::getTable('JobeetJob')->addActiveJobsQuery($q);
 
    return $q->fetchOne();
  }
 
  public function getExpiredJob()
  {
    $q = Doctrine_Query::create()
      ->from('JobeetJob j')
      ->where('j.expires_at < ?', date('Y-m-d', time()));
 
    return $q->fetchOne();
  }
}
<?php
// test/functional/frontend/categoryActionsTest.php
include(dirname(__FILE__).'/../../bootstrap/functional.php');
 
$browser = new JobeetTestFunctional(new sfBrowser());
 
$browser->info('1 - The category page')->
  info('  1.1 - Categories on homepage are clickable')->
  get('/')->
  click('Programming')->
  with('request')->begin()->
    isParameter('module', 'category')->
    isParameter('action', 'show')->
    isParameter('slug', 'programming')->
  end()->
 
  info(sprintf('  1.2 - Categories with more than %s jobs also have a "more" link', sfConfig::get('app_max_jobs_on_homepage')))->
  get('/')->
  click('27')->
  with('request')->begin()->
    isParameter('module', 'category')->
    isParameter('action', 'show')->
    isParameter('slug', 'programming')->
  end()->
 
  info(sprintf('  1.3 - Only %s jobs are listed', sfConfig::get('app_max_jobs_on_category')))->
  with('response')->checkElement('.jobs tr', sfConfig::get('app_max_jobs_on_category'))->
 
  info('  1.4 - The job listed is paginated')->
  with('response')->begin()->
    checkElement('.pagination_desc', '/32 jobs/')->
    checkElement('.pagination_desc', '#page 1/7#')->
  end()->
 
  click('2')->
  with('request')->begin()->
    isParameter('page', 2)->
  end()->
  with('response')->checkElement('.pagination_desc', '#page 2/7#')
;

テスト用のデータロード

$ ./symfony doctrine:data-load --dir="test/fixtures" --env="test"

デバッグ

テストに失敗したときに、どんなデータが返ってきているのかとか、現在のHTTPヘッダの状態とかを見るために debug() メソッドが用意されている

<?php
$browser->with('response')->debug();

テストコマンド

アプリケーション単位で行う

$ ./symfony test:functional frontend

単体テストと機能テストを同時に行う

$ ./symfony test:all



*1:原文ではclearCookie()だけど、恐らくtypo