symfony book 日本語ドキュメント

第15章 - ユニットテストと機能テスト

自動テストはプログラミングにおけるオブジェクト指向以降の最大の進歩の1つです。とりわけ、Webアプリケーションを開発するための助けになるので、例えおびただしい数のアプリケーションがリリースされたとしても、アプリケーションの品質を保証できます。symfonyは自動テストを円滑に運用するためのさまざまなツールを提供し、この章ではそれらを紹介します。

自動ツール

Webアプリケーションを開発した経験を持つ開発者はテストを実施するために時間がかかることを承知しています。テストケースを書き、それらを実施して、結果を解析することは退屈な作業です。加えて、Webアプリケーションの要件はつねに変化しがちなので、コードのリファクタリングとアプリケーションのリリースが継続して行われることになります。この作業の流れでは、予期しない新たなエラーが定期的に起こりがちです。

なぜ自動化されたツールが、必要ではなくても提案され、成功した開発環境の一部になっている理由はそういうわけです。テストケースのセットはアプリケーションが実際に行うことを保証します。内部のコードが頻繁に書き直される場合、自動化されたテストは予想外の回帰を防止します。加えて、厳格な標準フォーマットによって、テストフレームワークが理解しやすいようにテストを書くことを開発者に強制します。

自動テストは時に開発者のドキュメントに取って代わります。アプリケーションが行うことの説明になっているからです。よいテストスイートはテスト入力のセットのためにどんな出力が期待されているのかを示し、メソッドの目的を説明するよい方法です。

symfonyフレームワークはこの原則を自分自身に適用します。symfonyの内部は自動テストによって検証されます。これらのユニットテスト(unit test)と機能テスト(functional test)はsymfonyの標準的な配布物には搭載されていませんが、SVNリポジトリからチェックアウトするか、オンラインのhttp://trac.symfony-project.org/browser/branches/1.0/testで閲覧できます。

ユニットテストと機能テスト

ユニットテスト(unit test)は単一のコードコンポーネントが任意の入力に対して正しい出力を提供することを確認します。これらのテストは関数とメソッドがすべての特定のケースで動作する方法を検証します。ユニットテストは一度に1つのケースを処理するので、たとえば、1つのメソッドが特定の状況で異なる動作をする場合、いくつかのユニットテストが必要になることがあります。

機能テスト(functional test)は、シンプルな入力から出力への変換ではなく、完全な機能を検証します。たとえば、キャッシュシステムは機能テストだけで検証できます。なぜなら複数のステップが含まれるからです: 最初、ページがリクエストされ、レンダリングされます; つぎに、キャッシュからページが取得されます。ですので機能テストはプロセスを検証し、シナリオを必要とします。symfonyにおいて、すべてのアクションに対して機能テストを書くべきです。

もっと複雑なインタラクションのには、これらの2つのタイプのテストは不十分かもしれません。たとえば、AjaxのインタラクションはJavaScriptを実行するためにWebブラウザーを必要とするので、これらを自動的にテストするには特別なサードパーティのツールが必要です。さらに、視覚効果を検証できるのは人間だけです。

自動ツールへの広い範囲でのアプローチがある場合、おそらく、これらすべての方法の組み合わせを使う必要があります。指針としては、テストをシンプルで読みやすいものに保つべきであることを覚えておいてください。

NOTE 自動テストは結果と予期される出力の比較によって動作します。言い換えると、アサーション($a == 2などの式)を評価します。アサーションの値はtrueもしくはfalseで、テストが成功したか失敗したかを判定します。自動テストの技術を扱うとき"アサーション"(assertion)という言葉は一般的に使われます。

テスト駆動開発

テスト駆動開発(TDD - Test-Driven Development)の方法論において、テストはコードのまえに書かれます。最初にテストを書くことは実際に開発するまえに機能が実現するタスクに焦点を当てるための助けになります。これは、エクストリームプログラミング(XP Extreme Programming)のような、よい習慣で、同様にお勧めです。加えて、この方法論はユニットテストを最初に書いておかないとあとで書くことはないという事実を考慮しています。

たとえば、テキストをとり除く機能を開発しなければならない場合を考えてみましょう。この機能は文字列の最初と最後の空白スペースをとり除き、アルファベットでない文字をアンダースコアに置き換え、すべての大文字を小文字に変換します。テスト駆動開発において、テーブル15-1で示されるように、すべてのあり得る場合を考え、それぞれの場合に対して入力の例と期待される出力を準備することになります。

テーブル15-1 想定されるテキストをとり除く機能

入力 | 期待される出力t --------------------- | --------------------- " foo " | "foo" "foo bar" | "foo_bar" "-)foo:..=bar?" | "__foo____bar_" "FooBar" | "foobar" "Don't foo-bar me!" | "don_t_foo_bar_me_"

ユニットテストを書きたい場合、それらを実行して、失敗する様子を見てください。最初のテストケースを処理するために必要なコードを追加し、テストを再度動かし、最初のテストが成功するのを見て、そのように続けます。最終的に、すべてのテストケースは成功したとき、機能は正しいです。

テスト駆動方法論で開発されたアプリケーションは大まかに実際のコードと同じぐらいのテストコードで終わります。テストケースをデバッグすることに時間を費やしたくないのであれば、それらをシンプルに保ってください。

NOTE メソッドをリファクタリングすると以前は現れなかった新しいバグが作られる可能性があります。運用環境に新しいリリースのアプリケーションをデプロイするまえに、すべての自動テストを実行することもよい習慣であるのはそういうわけです。これは回帰テスト(regression testing)と呼ばれます。

limeテストフレームワーク

PHPの世界においてユニットテストのフレームワークは多く存在し、PhpUnitとSimpleTestがもっともよく知られています。symfonyはlimeと呼ばれる独自のテストフレームワークを持ちます。PerlライブラリのTest::Moreに基づき、TAP(Test Anything Protoco)に準拠しています、このことは、テストの出力をより読みやすくするために設計されたTAPで定められているように、テストの結果が表示されることを意味します。

limeはユニットテストのサポートを提供します。PHPのテストフレームワークよりも軽量でいくつかの利点があります:

つぎのセクションで説明されるさまざまなテストはlimeの構文を使います。symfonyをインストールしたのであればこれらのテストはそのまま動きます。

NOTE 運用サーバーでユニットテストと機能テストを起動させることは想定されていません。これらのテストは開発者のツールなので、ホストサーバーではなく、開発者のコンピュータで動かすべきです。

ユニットテスト

symfonyのユニットテストはTest.phpで終わるシンプルなPHPのファイルで、アプリケーションのtest/unit/ディレクトリに設置されています。これらはシンプルで読みやすい構文に従います。

ユニットテストは何に見えますか?

リスト15-1はstrtolower()関数のための典型的なユニットテストの一式を示しています。このテストはlime_testオブジェクトをインスタンス化することで始まります(今はパラメーターに悩む必要はありません)。それぞれのユニットテストはlime_testインスタンスへの呼び出しです。これらのメソッドの最後のパラメーターはつねに出力として提供されるオプションの文字列です。

リスト15-1 - ユニットテストのファイルの例(test/unit/strtolwerTest.php)

[php]
<?php

include(dirname(__FILE__).'/../bootstrap/unit.php');
require_once(dirname(__FILE__).'/../../lib/strtolower.php');

$t = new lime_test(7, new lime_output_color());

// strtolower()
$t->diag('strtolower()');
$t->isa_ok(strtolower('Foo'), 'string',
    'strtolower()は文字列を返す');
$t->is(strtolower('FOO'), 'foo',
    'strtolower()は入力を小文字に変換する');
$t->is(strtolower('foo'), 'foo',
    'strtolower()は小文字を変更しない');
$t->is(strtolower('12#?@~'), '12#?@~',
    'strtolower()はアルファベットではない文字を変更しない');
$t->is(strtolower('FOO BAR'), 'foo bar',
    'strtolower()は空白をそのままにする');
$t->is(strtolower('FoO bAr'), 'foo bar',
    'strtolower()は混合する文字の入力を扱う');
$t->is(strtolower(''), 'foo',
    'strtolower()は空の文字列をfooに変換する');

コマンドラインからtest-unitタスクでテストセットを起動させてください。コマンドラインの出力内容はとても明確なので、成功したテストと失敗したテストを見つけ出すための助けになります。リスト15-2の例のテストの出力をご覧ください。

リスト15-2 - 1つのユニットテストをコマンドラインから起動させる

> symfony test-unit strtolower

1..7
# strtolower()
ok 1 - strtolower()は文字列を返す
ok 2 - strtolower()は入力を小文字に変換する
ok 3 - strtolower()は小文字を変更しない
ok 4 - strtolower()はアルファベットではない文字を変更しない
ok 5 - strtolower()は空白をそのままにする
ok 6 - strtolower()は混合する文字の入力を扱う
not ok 7 - strtolower()は空の文字列をfooに変換する
#     Failed test (.\batch\test.php at line 21)
#            got: ''
#       expected: 'foo'
# Looks like you failed 1 tests of 7.

TIP リスト15-1の始めのincludeステートメントはオプションですが、php test/unit/strtolowerTest.phpを呼び出すことで、テストファイルはsymfonyのコマンドラインを使わずに実行できる独立したPHPのスクリプトになります。

ユニットテストのメソッド

テーブル15-2で一覧が示されるように、lime_testオブジェクトには多くのテストメソッドが付随しています。

テーブル15-2 - ユニットテストのためのlime_testオブジェクトのメソッド

メソッド | 説明 ------------------------------------------- | ------------------------ diag($msg) | コメントを出力するがテストは実施しない ok($test, $msg) | 条件をテストしてtrueの場合にパスする is($value1, $value2, $msg) | 2つの値を比較して等しい場合にパスする isnt($value1, $value2, $msg) | 2つの値を比較し、等しくない場合にパスする like($string, $regexp, $msg) | 正規表現に対して文字列をテストする unlike($string, $regexp, $msg) | 文字列が正規表現にマッチしないことをチェックする cmp_ok($value1, $operator, $value2, $msg) | 演算子で引数を比較する isa_ok($variable, $type, $msg) | 引数のタイプをチェックする isa_ok($object, $class, $msg) | オブジェクトのクラスをチェックする can_ok($object, $method, $msg) | オブジェクトもしくはクラスのためのメソッドが利用できるかチェックする is_deeply($array1, $array2, $msg) | 同じ値を持つ2つの配列をチェックする include_ok($file, $msg) | ファイルが存在し、適切に含まれるかをバリデートする fail() | つねに失敗します--テストの例外に便利です pass() | つねに成功します-- テストの例外に便利です skip($msg, $nb_tests) | $nb_tests件のテストをカウントします--条件つきのテストに便利です todo() | テストとしてカウントします-- まだ書かれていないテストに便利です

構文はとても単刀直入です; たいていのメソッドはメッセージを最後のパラメーターとして取ることに注意してください。このメッセージはテストが成功したときに出力に表示されます。これらのメソッドを学ぶベストな方法はこれらを実際にテストすることです。ですので、これらのメソッドをすべて使っているリスト15-3をご覧ください。

リスト15-3 - lime_testオブジェクトのメソッドをテストする(test/unit/exampleTest.php)

[php]
<?php

include(dirname(__FILE__).'/../bootstrap/unit.php');

// テストを目的としたスタブオブジェクトと関数
class myObject
{
  public function myMethod()
  {
  }
}

function throw_an_exception()
{
  throw new Exception('exception thrown');
}

// テストオブジェクトを初期化する
$t = new lime_test(16, new lime_output_color());

$t->diag('hello world');
$t->ok(1 == '1', '等号演算子は型を無視する');
$t->is(1, '1', '文字列は比較のために数字に変換される');
$t->isnt(0, 1, '0と1は等しくない');
$t->like('test01', '/test\d+/', 'test01はテストの番号付けパターンに従う');
$t->unlike('tests01', '/test\d+/', 'tests01はこのパターンに従わない');
$t->cmp_ok(1, '<', 2, '1は2より小さい');
$t->cmp_ok(1, '!==', true, '1とtrueはまったく同じではない');
$t->isa_ok('foobar', 'string', '\'foobar\'は文字列');
$t->isa_ok(new myObject(), 'myObject', 'new演算子は右のクラスのオブジェクトを作る');
$t->can_ok(new myObject(), 'myMethod', 'myObjectクラスのオブジェクトはmyMethodメソッドを持つ');
$array1 = array(1, 2, array(1 => 'foo', 'a' => '4'));
$t->is_deeply($array1, array(1, 2, array(1 => 'foo', 'a' => '4')),
    '最初と2番目の配列は同じ');
$t->include_ok('./fooBar.php', 'fooBar.phpファイルが適切にインクルードされた');

try
{
  throw_an_exception();
  $t->fail('例外が投じられた後コードは実行されません');
}
catch (Exception $e)
{
  $t->pass('例外の捕捉が成功しました');
}

if (!isset($foobar))
{
  $t->skip('テストの回数を正確に保つために1つのテストをスキップする', 1);
}
else
{
  $t->ok($foobar, 'foobar');
}

$t->todo('すべきテストが1つ残っている');

symfonyのユニットテスト内にこれらのメソッドの使いかたの例が多く見つかります。

TIP なぜok()と対照的にis()を使うのかとまどっているかもしれません。is()によるエラーメッセージの出力がはるかに明らかです; このメソッドがテストの両方のメンバを表示する一方でok()は条件が失敗したことを伝えます。

パラメーターをテストする

lime_testオブジェクトの初期化は実行されるテストの数を最初のパラメーターとしてとります。最終的に実行されるテストの数がこのパラメーターの数値と異なる場合、limeはそのことに関する警告を出力します。たとえば、リスト15-3のテストセットはリスト15-4のように出力します。16回のテストが行われることを保証しましたが、実際には15回だけ行われたので、出力はこれを示してします。

リスト15-4 - テスト実行回数のカウントはテストの計画の助けになる

> symfony test-unit example

1..16
# hello world
ok 1 - 等号演算子は型を無視する
ok 2 - 文字列は比較のために数字に変換される
ok 3 - 0と1は等しくない
ok 4 - test01はテストの番号付けパターンに従う
ok 5 - tests01はこのパターンに従わない
ok 6 - 1は2より小さい
ok 7 - 1とtrueはまったく同じではない
ok 8 - 'foobar'は文字列
ok 9 - new演算子は右のクラスのオブジェクトを作る
ok 10 - myObjectクラスのオブジェクトはamyMethodメソッドを持つ
ok 11 - 最初と2番目の配列は同じ
not ok 12 - fooBar.phpファイルが適切にインクルードされた
#     Failed test (.\test\unit\testTest.php at line 27)
#       Tried to include './fooBar.php'
ok 13 - 例外の捕捉が成功しました
ok 14 # SKIP テストの回数を正確に保つために1つのテストをスキップする
ok 15 # TODO すべきテストが1つ残っている
# Looks like you planned 16 tests but only ran 15.
# Looks like you failed 1 tests of 16.

diag()メソッドはテストとしてカウントされません。コメントを表示するためにこれを使用すれば、テストの出力は整理され読みやすい状態に保たれます。一方で、todo()メソッドとskip()メソッドは実際のテストとしてカウントされます。try/catchブロック内のpass()/fail()メソッドの組み合わせは単独のテストとしてカウントされます。

よく計画されたテスト戦略は予想されるテストの数を含まなければなりません。とりわけテストが内部の条件もしくは例外の条件で動作する複雑なケースにおいて、テストの数が独自のテストファイルを検証するためにとても便利であることがわかるでしょう。そして、テストがある時点で失敗するとすぐにテストの数がわかります。実行テストの最後の数が初期化の間に渡された数字と一致しないからです。

コンストラクターの2番目のパラメーターはlime_outputクラスを拡張する出力オブジェクトです。たいていの場合、テストはCLIを通して実行されることが前提なので、出力はlime_output_colorオブジェクトで、利用可能であればbashの色付けを活用します。

test-unitタスク

test-unitタスクは、コマンドラインから機能テストを起動させ、テストの名前のリストもしくはファイルのパターンを必要とします。リスト15-5で詳細をご覧ください。

リスト15-5 - 機能テストを起動させる

// testディレクトリ構造
test/
  unit/
    myFunctionTest.php
    mySecondFunctionTest.php
    foo/
      barTest.php

> symfony test-unit myFunction                   ## myFunctionTest.phpを実行する
> symfony test-unit myFunction mySecondFunction  ## 両方のテストを実行する
> symfony test-unit 'foo/*'                      ## barTest.phpを実行する
> symfony test-unit '*'                          ## すべてのテストを実行する(再帰的)

スタブ、フィクスチャ、オートロード

ユニットテストにおいて、デフォルトではオートロード機能は有効ではありません。テストで使うそれぞれのクラスはテストファイルで定義するか、外部依存のファイルとしてrequireステートメントで読み込まなければなりません。リスト15-6で示されるように、多くのテストファイルが複数行のincludeステートメントで始めるのはそういうわけです。

リスト15-6 - ユニットテストのクラスをインクルードする

[php]
<?php

include(dirname(__FILE__).'/../bootstrap/unit.php');
include(dirname(__FILE__).'/../../config/config.php');
require_once($sf_symfony_lib_dir.'/util/sfToolkit.class.php');

$t = new lime_test(7, new lime_output_color());

// isPathAbsolute()
$t->diag('isPathAbsolute()');
$t->is(sfToolkit::isPathAbsolute('/test'), true,
    'isPathAbsolute()が絶対パスであるならtrueを返す');
$t->is(sfToolkit::isPathAbsolute('\\test'), true,
    'isPathAbsolute()が絶対パスであるならtrueを返す');
$t->is(sfToolkit::isPathAbsolute('C:\\test'), true,
    'isPathAbsolute()が絶対パスであるならtrueを返す');
$t->is(sfToolkit::isPathAbsolute('d:/test'), true,
    'isPathAbsolute()が絶対パスであるならtrueを返す');
$t->is(sfToolkit::isPathAbsolute('test'), false,
    'isPathAbsolute()が相対パスであるならfalseを返す');
$t->is(sfToolkit::isPathAbsolute('../test'), false,
    'isPathAbsolute()が相対パスであるならfalseを返す');
$t->is(sfToolkit::isPathAbsolute('..\\test'), false,
    'isPathAbsolute()が相対パスであるならfalseを返す');

機能テストにおいて、テストしているオブジェクトだけをインスタンス化するだけでなく、依存するオブジェクトもインスタンス化する必要があります。機能テストは単一性を保たなければならないので、ほかのクラスに依存している場合1つのクラスが壊れると複数のテストが失敗する可能性があります。加えて、本当のオブジェクトをセットアップすることはコードの行数と実行時間の点から割高です。開発者は遅いプロセスにすぐに飽きるので、機能テストにおいてスピードが重大であることを覚えておいてください。

ユニットテストに対して多くのスクリプトをインクルードを始めるとき、シンプルなオートロードシステムが必要な場合があります。この目的のために、sfCoreクラス(手動でインクルードしなければなりません)は絶対パスをパラメーターとして必要とするinitSimpleAutoload()メソッドを提供します。このパスの元に設置されたすべてのクラスがオートロードされます。たとえば、$sf_symfony_lib_dir/util/の元に設置されたすべてのクラスをオートロードしたい場合、つぎのようなコードでユニットテストのスクリプトを始めてください。

[php]
require_once($sf_symfony_lib_dir.'/util/sfCore.class.php');
sfCore::initSimpleAutoload($sf_symfony_lib_dir.'/util');

TIP 生成されたPropelのオブジェクトはクラスの長いカスケードに依存するので、Propelを動作させたいのであれば、vendor/propel/ディレクトリのもとでファイルをインクルードする必要もあります(sfCoreへの呼び出しはsfCore::initSimpleAutoload(array(SF_ROOT_ DIR.'/lib/model', $sf_symfony_lib_dir.'/vendor/propel'));になります)。そして(set_include_path($sf_symfony_lib_dir.'/vendor'.PATH_SEPARATOR.SF_ROOT_DIR.PATH_SEPARATOR.get_include_path()を呼び出すことで)Propelのコアをincludeのパスに追加する必要があります。

オートロードの問題に関する別のよい次善策はスタブを利用することです。スタブ(stub)は本当のメソッドがシンプルであらかじめ用意されたデータに置き換わるクラスの代替の実装です。これは本当のクラスのふるまいを真似しますが、負担はかかりません。スタブのよい例はデータベースへの接続やWebサービスのインターフェイスです。リスト15-7において、APIマッピングのためのユニットテストはWebServiceクラスに依存します。実際のWebサービスクラスの本当のfetch()メソッドを呼び出す代わりに、テストはテストデータを返すスタブを使います。

リスト15-7 - ユニットテストでスタブを使う

[php]
require_once(dirname(__FILE__).'/../../lib/WebService.class.php');
require_once(dirname(__FILE__).'/../../lib/MapAPI.class.php');

class testWebService extends WebService
{
  public static function fetch()
  {
    return file_get_contents(dirname(__FILE__).'/fixtures/data/fake_web_service.xml');
  }
}

$myMap = new MapAPI();

$t = new lime_test(1, new lime_output_color());

$t->is($myMap->getMapSize(testWebService::fetch(), 100));

テストデータは文字列やメソッドへの呼び出しよりも複雑になる可能性があります。複雑なテストデータはしばしフィクスチャ(fixture - 付属品)として参照されます。コーディングを明快にするために、とりわけフィクスチャが複数のユニットテストのファイルによって使われる場合、フィクスチャを別々のファイルに保存したほうがベターです。symfonyがsfYAML::load()メソッドによってYAMLファイルを配列に簡単に変換できることも忘れないでください。リスト15-8のように、このことはPHPの長い配列を書く代わりに、YAMLのファイルでテストデータを書くことができることを意味します。

リスト15-8 - ユニットテストでフィクスチャファイルを使う

[php]
// fixtures.ymlにて:
-
  input:   '/test'
  output:  true
  comment: isPathAbsolute()が絶対パスである場合trueを返す
-
  input:   '\\test'
  output:  true
  comment: isPathAbsolute()が絶対パスである場合trueを返す
-
  input:   'C:\\test'
  output:  true
  comment: isPathAbsolute()が絶対パスである場合trueを返す
-
  input:   'd:/test'
  output:  true
  comment: isPathAbsolute()が絶対パスである場合trueを返す
-
  input:   'test'
  output:  false
  comment: isPathAbsolute()が相対パスである場合falseを返す
-
  input:   '../test'
  output:  false
  comment: isPathAbsolute()が相対パスである場合falseを返す
-
  input:   '..\\test'
  output:  false
  comment: isPathAbsolute()が相対パスである場合falseを返す

// testTest.phpにて
<?php

include(dirname(__FILE__).'/../bootstrap/unit.php');
include(dirname(__FILE__).'/../../config/config.php');
require_once($sf_symfony_lib_dir.'/util/sfToolkit.class.php');
require_once($sf_symfony_lib_dir.'/util/sfYaml.class.php');

$testCases = sfYaml::load(dirname(__FILE__).'/fixtures.yml');

$t = new lime_test(count($testCases), new lime_output_color());

// isPathAbsolute()
$t->diag('isPathAbsolute()');
foreach ($testCases as $case)
{
  $t->is(sfToolkit::isPathAbsolute($case['input']), $case['output'],$case['comment']);
}

機能テスト

機能テスト(functional test)はアプリケーションの一部を検証します。これらのテストは、アクションが想定された動作をするか手作業で検証する方法と同じように、ブラウジングセッションをシミュレートし、リクエストを作り、レスポンスの要素をチェックします。

機能テストはどのように見えますか?

テキストブラウザーと多くの正規表現で機能テストを実行できますが、時間の大きな無駄遣いです。symfonyはsfBrowserという名前の特別なオブジェクトを提供します。このオブジェクトは実際に必要なサーバーをともなわずにsymfonyのアプリケーションに接続したブラウザーのようにふるまいます。そしてHTTPの転送の減速は起きません。このオブジェクトはそれぞれのリクエスト(リクエスト、セッション、コンテキスト、レスポンスオブジェクト)のコアオブジェクトにアクセスできます。symfonyはTestBrowserと呼ばれるこのクラスの拡張機能も提供します。sfTestBrowserは機能テストのために設計され、sfBrowserオブジェクトのすべての機能に加えてスマートなアサートメソッドを持ちます。

機能テストは伝統的にテストブラウザーのオブジェクトを初期化することで始まります。このオブジェクトはリアクションへのレスポンスを作成し、レスポンス内に存在するいくつかの要素を変更します。

たとえば、init-moduleタスクもしくはpropel-init-crudタスクでモジュールスケルトンを生成するたびに、symfonyはこのモジュールのためにシンプルな機能テストを作ります。テストはモジュールのデフォルトアクションにリクエストを行いレスポンスのステータスコード、ルーティングシステムによって算出されたモジュールとアクション、とレスポンスの内容のなかの特定のセンテンスの存在をチェックします。foobarモジュールに対して、生成されたfoobarActionsTest.phpファイルはリスト15-9のようになります。

リスト15-9 - 新しいモジュールのためのデフォルトの機能テスト(tests/functional/frontend/foobarActionsTest.php)

[php]
<?php

include(dirname(__FILE__).'/../../bootstrap/functional.php');

// 新しいテストブラウザーを作成する
$browser = new sfTestBrowser();
$browser->initialize();

$browser->
  get('/foobar/index')->
  isStatusCode(200)->
  isRequestParameter('module', 'foobar')->
  isRequestParameter('action', 'index')->
  checkResponseElement('body', '!/This is a temporary page/')
;

TIP ブラウザーのメソッドはsfTestBrowserオブジェクトを返すので、テストファイルをより読みやすくするにはメソッドチェーンを利用できます。これはオブジェクトへの流れるようなインターフェイス(fluid interfaceもしくはfluent interface)と呼ばれます。この名前の由来はメソッド呼び出しの流れを止めるものがないからです。

機能テストはいくつかのリクエストと複雑なアサーションを含むことができます; つぎのセクションですべての機能を見ることになります。

機能テストを立ち上げるために、リスト15-10で示されるように、symfonyのコマンドラインでtest-functionalタスクを使います。このタスクはアプリケーションの名前とテストの名前を必要とします(Test.phpのサフィックスを出力します)。

リスト15-10 - コマンドラインから1つの機能テストを立ち上げる

> symfony test-functional frontend foobarActions

# get /comment/index
ok 1 - status code is 200
ok 2 - request parameter module is foobar
ok 3 - request parameter action is index
not ok 4 - response selector body does not match regex /This is a temporary page/
# Looks like you failed 1 tests of 4.
1..4

新しいモジュールに対して生成された機能テストはデフォルトでは成功しません。新しく作成されたモジュールにおいて、indexアクションは、"This is a temporary page."という文を含む初期ページにフォワードします(symfonyのdefaultモジュールを含みます)。indexアクションを修正しないかぎり、このモジュールに対するテストは失敗します。これは未終了のモジュールですべてのテストを成功できないことを保証します。

NOTE 機能テストにおいて、オートロードが有効なので、手動でファイルをインクルードする必要はありません。

sfBrowserオブジェクトでブラウジングする

テストブラウザーはGETリクエストとPOSTリクエストを行う機能を持ちます。両方の場合において、本当のURIをパラメーターとして使います。リスト15-11はリクエストをシミュレートするためにsfTestBrowserオブジェクトへの呼び出しを書く方法を示しています。

リスト15-11 - sfTestBrowserオブジェクトでリクエストをシミュレートする

[php]
include(dirname(__FILE__).'/../../bootstrap/functional.php');

// 新しいテストブラウザーを作成する
$b = new sfTestBrowser();
$b->initialize();

$b->get('/foobar/show/id/1');                   // GETリクエスト
$b->post('/foobar/show', array('id' => 1));     // POSTリクエスト

// get()メソッドとpost()メソッドはcall()メソッドへのショートカット
$b->call('/foobar/show/id/1', 'get');
$b->call('/foobar/show', 'post', array('id' => 1));

// call()メソッドは任意のメソッドによるリクエストをシミュレートする
$b->call('/foobar/show/id/1', 'head');
$b->call('/foobar/add/id/1', 'put');
$b->call('/foobar/delete/id/1', 'delete');

典型的なブラウジングセッションは特定のアクションへのリクエストだけでなく、リンクとブラウザーボタンへのクリックも含みます。リスト15-12で示されるように、sfTestBrowserオブジェクトはこれらもシミュレートできます。

リスト15-12 - sfTestBrowserオブジェクトでナビゲーションをシミュレートする

[php]
$b->get('/');                  // ホームページへのリクエスト
$b->get('/foobar/show/id/1');
$b->back();                    // 履歴の1つのページに戻る
$b->forward();                 // 履歴の1つのページに進む
$b->reload();                  // 現在のページをリロードする
$b->click('go');               // 'go'リンクもしくはボタンを探してクリックする

テストブラウザーは呼び出しのスタックを処理するので、back()メソッドとforward()メソッドは本当のブラウザー上と同じように動作します。

TIP テストブラウザーはセッション(sfTestStorage)とCookieを管理する独自のメカニズムを持ちます。

テストする必要のあるインタラクションのなかで、おそらくフォームに関連するものがもっとも優先されます。フォームの入力と投稿をシミュレートするには、選択肢が3つあります。送信したいパラメーターでPOSTリクエストを行う場合、配列としてのformパラメーターでclick()を呼び出すか、1つずつフィールドを入力して、投稿ボタンをクリックします。いずれにせよ、これらはすべて同じPOSTリクエストになります。リスト15-13は例を示しています。

リスト15-13 - sfTestBrowserオブジェクトでフォーム入力をシミュレートする

[php]
// modules/foobar/templates/editSuccess.phpでのテンプレートの例
<?php echo form_tag('foobar/update') ?>
  <?php echo input_hidden_tag('id', $sf_params->get('id')) ?>
  <?php echo input_tag('name', 'foo') ?>
  <?php echo submit_tag('go') ?>
  <?php echo textarea('text1', 'foo') ?>
  <?php echo textarea('text2', 'bar') ?>
</form>

// このフォームのための機能テストの例
$b = new sfTestBrowser();
$b->initialize();
$b->get('/foobar/edit/id/1');

// オプション 1: POSTリクエスト
$b->post('/foobar/update', array('id' => 1, 'name' => 'dummy', 'commit' => 'go'));

// オプション 2: パラメーターで投稿ボタンをクリックする
$b->click('go', array('name' => 'dummy'));

// オプション 3: フィールド名でフォームの値を入力し投稿ボタンをクリックする
$b->setField('name', 'dummy')->
    click('go');

NOTE 2番目と3番目のオプションによって、デフォルトのフォームの値は自動的にフォームの投稿に含まれ、フォームターゲットを指定する必要はありません。

redirect()メソッドによってアクションが終了した場合、テストブラウザーは自動的にリダイレクトされません; リスト15-14でお手本が示されるように、手動によるfollowRedirect()メソッドでテストブラウザーをリダイレクトします。

リスト15-14 - テストブラウザーは自動的にリダイレクトされない

[php]
// modules/foobar/actions/actions.class.phpのアクションの例
public function executeUpdate()
{
  ...
  $this->redirect('foobar/show?id='.$this->getRequestParameter('id'));
}

// このアクションのための機能テストの例
$b = new sfTestBrowser();
$b->initialize();
$b->get('/foobar/edit?id=1')->
    click('go', array('name' => 'dummy'))->
    isRedirected()->   // リクエストがリダイレクトされたかチェックする
    followRedirect();    // 手動でリダイレクトの後に続く

ブラウジングのために便利なメソッドが1つ残っています。restart()はあたかもブラウザーを再起動したようにブラウジングの履歴、セッションとCookieを再び初期化します。

このメソッドが最初のリクエストを行うと、sfTestBrowserオブジェクトはリクエスト、コンテキスト、レスポンスオブジェクトにアクセスできます。テキストの内容からレスポンスヘッダー、リクエストパラメーターと設定まで及ぶ、多くの内容をチェックできます:

[php]
$request  = $b->getRequest();
$context  = $b->getContext();
$response = $b->getResponse();

SIDEBAR sfBrowserオブジェクト

リスト15-10から15-13まで説明されたすべてのブラウジングメソッドはsfBrowserオブジェクト全体に渡り、テストの範囲からも利用可能でつぎのように呼び出すことができます:

[php]
// 新しいブラウザーを作成する
$b = new sfBrowser();
$b->initialize();
$b->get('/foobar/show/id/1')->
    setField('name', 'dummy')->
    click('go');
$content = $b->getResponse()->getContent();
...

たとえば、それぞれのバッチスクリプトに対してキャッシュバージョンを生成するためにページのリストをブラウジングしたい場合、sfBrowserオブジェクトはバッチスクリプトのためにとても便利なツールです。(詳細な例に関しては18章を参照)。

アサーションを使う

レスポンスとリクエストのほかのコンポーネントにアクセスできるsfTestBrowserオブジェクトのおかげで、これらのコンポーネント上でテストを実施できます。この目的のために新しいlime_testオブジェクトを作成できますが、幸いにして、sfTestBrowserlime_testオブジェクトを返すtest()メソッドを提示します。sfTestBrowser経由でアサーションを行う方法に関してはリスト15-15で確認してください。

リスト15-15 - テストブラウザーはtest()メソッドによるテスト機能を提供する

[php]
$b = new sfTestBrowser();
$b->initialize();
$b->get('/foobar/edit/id/1');
$request  = $b->getRequest();
$context  = $b->getContext();
$response = $b->getResponse();

// test()メソッドを通してlime_testメソッドにアクセスする
$b->test()->is($request->getParameter('id'), 1);
$b->test()->is($response->getStatuscode(), 200);
$b->test()->is($response->getHttpHeader('content-type'), 'text/html;charset=utf-8');
$b->test()->like($response->getContent(), '/edit/');

NOTE getResponse()getContent()getRquest()と、test()メソッドはsfBrowserオブジェクトを返さないので、これらのあとではsfTestBrwoserメソッド呼び出しのチェーンを使用できません。

リスト15-16で示されるように、リクエストオブジェクトとレスポンスオブジェクトを通して新旧のCookieをチェックできます。

リスト15-16 - sfTestBrowserでCookieをテストする

[php]
$b->test()->is($request->getCookie('foo'), 'bar');     // 入ってくるCookie
$cookies = $response->getCookies();
$b->test()->is($cookies['foo'], 'foo=bar');            // 出て行くCookie

リクエストの要素をテストするためにtest()メソッドを使うと長い行のコードを書くことになります。幸いにして、sfTestbrowserオブジェクトは機能テストを読みやすく短く保つ一連のプロキシメソッドを含みます。さらに、これらのメソッドはこれら自身でsfTestBrowserオブジェクトを返します。たとえば、リスト15-17で示されるように、リスト15-15をより速い方法で書き換えることができます。

リスト15-17 - sfTestBrowserで直接テストする

[php]
$b = new sfTestBrowser();
$b->initialize();
$b->get('/foobar/edit/id/1')->
    isRequestParameter('id', 1)->
    isStatusCode()->
    isResponseHeader('content-type', 'text/html; charset=utf-8')->
    responseContains('edit');

ステータス200はisStatusCode()メソッドによって求められるパラメーターのデフォルト値なので、連続したレスポンスをテストするために引数なしでこのメソッドを呼び出すことができます。

プロキシメソッドの利点はlime_testメソッドで出力テキストを指定する必要がないことです。メッセージはプロキシメソッドによって自動的に生成され、テストの出力は明快で読みやすいです。

# get /foobar/edit/id/1
ok 1 - request parameter "id" is "1"
ok 2 - status code is "200"
ok 3 - response header "content-type" is "text/html"
ok 4 - response contains "edit"
1..4

実際には、リスト15-17のプロキシメソッドは通常のテストの大部分をカバーするので、sfTestBrowserオブジェクト上でtest()メソッドを使うことはめったにありません。

リスト15-14はsfTestBrowserは自動的にリダイレクトの後に続かないことを示しました。これは1つの利点を持ちます: リダイレクトをテストできることです。たとえば、リスト15-18はリスト15-14のレスポンスをテストする方法を示しています。

リスト15-18 - sfTestBrowserでリダイレクトをテストする

[php]
$b = new sfTestBrowser();
$b->initialize();
$b->
    get('/foobar/edit/id/1')->
    click('go', array('name' => 'dummy'))->
    isStatusCode(200)->
    isRequestParameter('module', 'foobar')->
    isRequestParameter('action', 'update')->

    isRedirected()->      // レスポンスがリダイレクトであることを確認する
    followRedirect()->    // 手動でリダイレクトをフォローする     

    isStatusCode(200)->
    isRequestParameter('module', 'foobar')->
    isRequestParameter('action', 'show');

CSSセレクタを使う

多くの機能テストはコンテンツ内にテキストが存在することを確認することでページが正しいかを検証します。responseContains()メソッド内で正規表現の助けを借りることで、表示されるテキスト、タグの属性、もしくは値をチェックできます。しかし、レスポンスのDOMに深く埋め込まれたものをチェックしたいのであれば、正規表現は理想的な方法ではありません。

sfTestBrowserオブジェクトがgetResponseDom()メソッドをサポートするわけはそういうわけです。これはlibXML2のDOMオブジェクトを返し、解析とテストの実行はフラットなテキストよりもはるかに簡単です。このメソッドの使いかたの例はリスト15-19をご覧ください。

リスト15-19 - テストブラウザーはDOMオブジェクトとしてレスポンスの内容にアクセスできる

[php]
$b = new sfTestBrowser();
$b->initialize();
$b->get('/foobar/edit/id/1');
$dom = $b->getResponseDom();
$b->test()->is($dom->getElementsByTagName('input')->item(1)->getAttribute('type'),'text');

PHPのDOMメソッドによるHTMLのドキュメントの解析は十分な速さで行われずまた簡単でもありません。CSSセレクタに慣れているのであれば、これらのセレクタがHTMLのドキュメントから要素を読みとるためのより強力な方法であることをご存じでしょう。symfonyはDOMドキュメントをコンストラクターのパラメーターとして必要とするsfDomCssSelectorと呼ばれるツールクラスを提供します。これはCSSセレクタにしたがって文字列の配列を返すgetTexts()メソッドと、DOM要素の配列を返すgetElements()メソッドを持ちます。リスト15-20の例をご覧ください。

リスト15-20 - テストブラウザーはsfDomCssSelectorオブジェクトとしてのレスポンスの内容にアクセスできる

[php]
$b = new sfTestBrowser();
$b->initialize();
$b->get('/foobar/edit/id/1');
$c = new sfDomCssSelector($b->getResponseDom())
$b->test()->is($c->getTexts('form input[type="hidden"][value="1"]'), array('');
$b->test()->is($c->getTexts('form textarea[name="text1"]'), array('foo'));
$b->test()->is($c->getTexts('form input[type="submit"]'), array(''));

簡潔さと明瞭さを絶えず追求するために、symfonyはショートカットを提供します: checkRsponseElement()プロキシメソッドです。このメソッドはリスト15-20の内容をリスト15-21のようにします。

リスト15-21 - テストブラウザーはCSSセレクタによってレスポンス要素にアクセスできる

[php]
$b = new sfTestBrowser();
$b->initialize();
$b->get('/foobar/edit/id/1')->
    checkResponseElement('form input[type="hidden"][value="1"]', true)->
    checkResponseElement('form textarea[name="text1"]', 'foo')->
    checkResponseElement('form input[type="submit"]', 1);

checkResponseElement()メソッドのふるまいはそれが受けとる2番目の引数の型に依存します:

メソッドは3番目のオプションパラメーターを連想配列の形式で受けとります。リスト15-22で示されるように、(セレクタがいくつかの要素を返す場合)セレクタによって返された最初の要素上ではなく、特定の位置上のほかの要素上でテストが実行されます。

リスト15-22 - 特定の位置で要素にマッチする位置オプションを使う

[php]
$b = new sfTestBrowser();
$b->initialize();
$b->get('/foobar/edit?id=1')->
    checkResponseElement('form textarea', 'foo')->
    checkResponseElement('form textarea', 'bar', array('position' => 1));

オプションの配列は2つのテストを同時に実施するためにも使われます。リスト15-23で示されるように、セレクタが要素にマッチするかどうかとそれらが存在する数に関してテストできます。

リスト15-23 - マッチする数をカウントするcountオプションを使う

[php]
$b = new sfTestBrowser();
$b->initialize();
$b->get('/foobar/edit?id=1')->
    checkResponseElement('form input', true, array('count' => 3));

セレクタのツールはとても強力です。これはCSS2.1のセレクタの大部分を受け入れ、リスト15-24のような複雑なクエリに対して利用できます。

リスト15-24 - checkResponseElment()によって受け入れられる複雑なCSSセレクタの例

[php]
$b->checkResponseElement('ul#list li a[href]', 'click me');
$b->checkResponseElement('ul > li', 'click me');
$b->checkResponseElement('ul + li', 'click me');
$b->checkResponseElement('h1, h2', 'click me');
$b->checkResponseElement('a[class$="foo"][href*="bar.html"]', 'my link');

テスト環境でとり組む

sfTestBrowserオブジェクトはtest環境内で設定される特別なフロントコントローラーを使います。この環境に対するデフォルト設定はリスト15-25で表されます。

リスト15-25 - テスト環境のデフォルト設定(myapp/config/settings.php)

test:
  .settings:
    # E_ALL | E_STRICT & ~E_NOTICE = 2047
    error_reporting:        2047
    cache:                  off
    web_debug:              off
    no_script_name:         off
    etag:                   off

この環境においてキャッシュ(cache)とWebデバッグツールバー(web_debug)はoffに設定されます。しかしながら、コードの実行は、dev環境とprod環境のログファイルは別にして、ログファイルにトレースされているので、それぞれのファイルを個別に確認できます(myproject/log/myapp_test.log)。この環境において、例外はスクリプトの実行を中止しません。1つのテストが失敗してもテスト全体のセットを実施できます。たとえば、テストデータを持つほかのデータベースを使うために、個別のデータベースの設定を持つことができます。

sfTestBrowserオブジェクトは使うまえに初期化しなければなりません。必要であれば、アプリケーションのためのホスト名とクライアントのためのIPアドレスを指定できます。すなわち、これら2つのパラメーターを通してアプリケーションがコントロールを行う場合です。リスト15-26はこの方法を示しています。

リスト15-26 - ホスト名とIPでテストブラウザーをセットアップする

[php]
$b = new sfTestBrowser();
$b->initialize('myapp.example.com', '123.456.789.123');

test-functionalタスクを使う

test-functionalタスクによって1つもしくは複数の機能テストを実施することが可能で、このタスクは受けとる引数の数に依存します。リスト15-27で示されるように、機能テストが最初の引数としてアプリケーションの名前を必要とすること以外、ルールはtest-unitタスクのものと同じになります。

リスト15-27 - 機能テストのタスク構文

// testディレクトリの構造
test/
  functional/
    frontend/
      myModuleActionsTest.php
      myScenarioTest.php
    backend/
      myOtherScenarioTest.php

## 再帰的に、1つのアプリケーションに対してすべての機能テストを実行する
> symfony test-functional frontend

## 1つの任意の機能テストを実行する
> symfony test-functional frontend myScenario

## パターンに基づいていくつかのテストを実行する
> symfony test-functional frontend my*

テストの命名慣習

このセクションではテストを整理して維持しやすい状態に保つためのいくつかの慣習の一覧を示します。使いこなすための秘訣はファイルの整理、ユニットテストと機能テストに関することです。

ファイル構造に関しては、テストする予定のクラス名で機能テストのファイルを名づけ、テストする予定のモジュールもしくはシナリオの名前でユニットテストを名づけます。例としてリスト15-28をご覧ください。test/ディレクトリはすぐに多くのファイルを含むようになるので、これらのガイドラインに従わないと、長い間にテストを見つけることが困難になる可能性があります。

リスト15-28 - ファイルの命名慣習の例

test/
  unit/
    myFunctionTest.php
    mySecondFunctionTest.php
    foo/
      barTest.php
  functional/
    frontend/
      myModuleActionsTest.php
      myScenarioTest.php
    backend/
      myOtherScenarioTest.php

ユニットテストのためのよい習慣は関数もしくはメソッドによってテストを分類することとdiag()呼び出しでそれぞれのテストのグループを始めることです。それぞれの機能テストのメッセージは関数の名前もしくは、テストされたメソッドを含み、動詞とプロパティの後に続くので、テストの出力はオブジェクトのプロパティを説明する文のように見えます。リスト15-29は例を示しています。

リスト15-29 - ユニットテストの命名慣習の例

[php]
// srttolower()
$t->diag('strtolower()');
$t->isa_ok(strtolower('Foo'), 'string', 'strtolower()は文字列を返す');
$t->is(strtolower('FOO'), 'foo', 'strtolower()は入力を小文字に変換する');

# strtolower()
ok 1 - strtolower()は文字列を返す
ok 2 - strtolower()は入力を小文字に変換する

機能テストはページによって分類されリクエストによって始まります。リスト15-30はこの慣習を説明しています。

リスト15-30 - 機能テストの命名慣習の例

[php]
$browser->
  get('/foobar/index')->
  isStatusCode(200)->
  isRequestParameter('module', 'foobar')->
  isRequestParameter('action', 'index')->
  checkResponseElement('body', '/foobar/')
;

# /comment/indexを取得する
ok 1 - status code is 200
ok 2 - request parameter module is foobar
ok 3 - request parameter action is index
ok 4 - response selector body matches regex /foobar/

この規約に従えば、プロジェクトの開発者のドキュメントとして使うさいにテストの出力は十分に明快なものになります。そしていくつかの場合においてドキュメントを実際に書かなくてもすみます。

特別なテストのニーズ

たいていの場合、symfonyによって提供されたユニットテストと機能テストのツールで十分です。自動テストにおける共通の問題を解決するためのいくつかの補足のテクニックの一覧をこのセクションに書いておきます: 孤立した環境でテストの立ち上げ、テストの範囲以内でデータベースにアクセスし、キャッシュのテスト、クライアントサイド上でインタラクションのテストを行うことです。

テストハーネスでテストを実行する

test-unittest-functionalタスクは単独のテストもしくはテストのセットを立ち上げることができます。しかしながら、これらのタスクをパラメーターなしで呼び出す場合、これらはtest/ディレクトリ内に書かれたすべてのユニットテストと機能テストを立ち上げます。テストの間の汚染を回避するには、それぞれのテストファイルを独立したサンドボックスに分離する特定のメカニズムが必要です。さらに、(出力は何千行の長さになるので)その場合、単独のテストファイルのように同じ出力を続けることは無意味なので、テストの結果は統合的なビューにまとめられます。これが多くのテストファイルを実行するためにテストハーネスを使う理由です。テストハーネス(test harness)は特別な機能を持つ自動テストフレームワークです。テストハーネスはlime_harnessと呼ばれるlimeフレームワークのコンポーネントに依存しています。リスト15-31のように、これはファイルごとのテストの状態と終了したテストの数の概要を示します。

リスト15-31 - テストハーネスですべてのテストを立ち上げる

> symfony test-all

unit/myFunctionTest.php................ok
unit/mySecondFunctionTest.php..........ok
unit/foo/barTest.php...................not ok

Failed Test                     Stat  Total   Fail  List of Failed
------------------------------------------------------------------
unit/foo/barTest.php               0      2      2  62 63
Failed 1/3 test scripts, 66.66% okay. 2/53 subtests failed, 96.22% okay.

テストは1つずつ呼び出すときと同じ方法で実行されます; 本当に便利にするために出力だけが短くなります。とりわけ、最後の表は失敗したテストに焦点を当てているので、これらのテストを見つけるための助けになります。

リスト15-32で示されるように、テストハーネスのtest-allタスクを使うことですべてのテストを1つの呼び出しで起動できます。最新のリリース以降でリグレッション(回帰)が起こらないことを保証するために、この呼び出しはすべてのコードを製品環境に転送するまえに行うべきです。

リスト15-32 - プロジェクトのすべてのテストを立ち上げる

> symfony test-all

データベースにアクセスする

ユニットテストにおいてデータベースにアクセスすることがよく必要になります。最初にsfTestBrowser::get()を呼び出すときにデータベース接続は自動的に初期化されます。しかしながら、sfTestBrowserを使うまえにもデータベースに接続したい場合、リスト15-33のように、手動でsfDabataseManagerを初期化しなければなりません。

リスト15-33 - テストにおいてデータベースを初期化する

[php]
$databaseManager = new sfDatabaseManager();
$databaseManager->initialize();

// オプションとして、現在のデータベース接続を取得できる
$con = Propel::getConnection();

テストを始めるまえにデータベースにフィクスチャを投入します。これはsfPropelDataオブジェクトを通して行うことができます。リスト15-34で示されるように、propel-load-dataタスクのように、ファイルからもしくは配列から、このオブジェクトはデータをロードします。

リスト15-34 - テストファイルからデータベースに投入する

[php]
$data = new sfPropelData();

// ファイルからデータをロードする
$data->loadData(sfConfig::get('sf_data_dir').'/fixtures/test_data.yml');

// 配列からデータをロードする
$fixtures = array(
  'Article' => array(
    'article_1' => array(
      'title'      => 'foo title',
      'body'       => 'bar body',
      'created_at' => time(),
    ),
    'article_2'    => array(
      'title'      => 'foo foo title',
      'body'       => 'bar bar body',
      'created_at' => time(),
    ),
  ),
);
$data->loadDataFromArray($fixtures);

それから、あなたのテストのニーズに合わせて通常のアプリケーションのようにPropelオブジェクトを使います。これらのファイルをユニットテストにインクルードすることを覚えておいてください(この章の前のセクションの"スタブ、フィクスチャ、オートロード"で説明されているように、テストを自動化するためにsfCore::sfSimpleAutoloading()メソッドを使用できます)。Propelオブジェクトは機能テストにオートロードされます。

キャッシュをテストする

アプリケーションに対してキャッシュを有効にしたとき、機能テストは期待どおりにキャッシュされたアクションが動作するか検証します。

最初に行うべきことはテスト環境(settings.ymlファイル)に対してキャッシュを有効にすることです。それから、ページがキャッシュから由来するものなのか、生成されたものであるのかをテストしたい場合、sfTestBrowserオブジェクトが提供するisCached()テストメソッドを使います。リスト15-35このメソッドの使いかたを示しています。

リスト15-35 - isCached()メソッドはキャッシュをテストする

[php]
<?php

include(dirname(__FILE__).'/../../bootstrap/functional.php');

// 新しいテストブラウザーを作成する
$b = new sfTestBrowser();
$b->initialize();

$b->get('/mymodule');
$b->isCached(true);       // レスポンスがキャッシュからやって来たことを確認する
$b->isCached(true, true); // キャッシュされたレスポンスがレイアウトと一緒に来ることを確認する
$b->isCached(false);      // レスポンスがキャッシュからやって来ないことを確認する

NOTE 機能テストの最初にキャッシュをクリアする必要はありません; ブートストラップのスクリプトが代行してくれます。

クライアント上のインタラクションをテストする

以前説明されたテクニックの主な難点はJavaScriptをシミュレートできないことです。たとえば、Ajaxインタラクションのようなとても複雑なインタラクションのために、ユーザーが行うマウスとキーボードの入力とクライアントサイド上でのスクリプトの実行を再現できることが必要です。通常、これらのテストは手作業で再現されますが、とても時間がかかりエラーになりがちです。

解決方法はSelenium(http://www.openqa.org/selenium/)と呼ばれるもので、完全にJavaScriptで書かれたテストフレームワークです。このツールは、現在のブラウザーウィンドウを利用して、通常のユーザーが行うようなページ上のアクションのセットを実行します。sfBrowserオブジェクトを越える利点はSlemeniumがページ内でJavaScriptを実行できるので、AjaxインタラクションもSlemeniumでテストできることです。

symfonyはSeleniumをデフォルトで搭載していません。これをインストールするには、web/ディレクトリ内に新しくselenium/ディレクトリを作り、Seleniumアーカイブの内容を展開する必要があります(http://www.openqa.org/selenium-core/download.action)。なぜなら、SeleniumはJavaScriptに依存するので、たいていのブラウザー内のセキュリティ設定の基準に従えば、アプリケーションに関して同じホストとポート上でJavaScriptが利用できないかぎり、Seleniumの動作が許可されないからです。

CAUTION selenium/ディレクトリを運用サーバーに直接転送しないように気をつけてください。ブラウザーを通してWebドキュメントのrootに誰でもアクセスできるからです。

SeleniumテストはHTML形式で書かれweb/slenium/tests/ディレクトリに保存されます。たとえば、リスト15-36は、ホームページがロードされ、click meのリンクがクリックされ、レスポンス内で"Hello, World"のテキストが探される機能テストを示します。テスト環境内でアプリケーションにアクセスするには、myapp_test.phpフロントコントローラーを指定する必要があります。

リスト15-36 - Seleniumテストのサンプル(web/selenium/test/testIndex.html)

[php]
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
  <meta content="text/html; charset=UTF-8" http-equiv="content-type">
  <title>Index tests</title>
</head>
<body>
<table cellspacing="0">
<tbody>
  <tr><td colspan="3">First step</td></tr>
  <tr><td>open</td>              <td>/myapp_test.php/</td> <td>&nbsp;</td></tr>
  <tr><td>clickAndWait</td>      <td>link=click me</td>    <td>&nbsp;</td></tr>
  <tr><td>assertTextPresent</td> <td>Hello, World!</td>    <td>&nbsp;</td></tr>
</tbody>
</table>
</body>
</html>

テストケースはコマンド、ターゲット、値の3つのカラムを持つテーブルを含むHTMLドキュメントによって表現されます。すべてのコマンドは値をとりません。コマンドが値を取らない場合、カラムを空白にしておくか、テーブルを見やすくするために&nbsp;を使うことです。コマンドの完全な一覧はSeleniumのWebサイトを参照してください。

同じディレクトリに設置されたTestSuite.htmlファイル内に新しい行を挿入することで、このテストをグローバルテストスイートに追加する必要があります。リスト15-37はこれを行う方法を示しています。

リスト15-37 - テストファイルをテストスイートに追加する(web/selenium/test/TestSuite.html)

...
<tr><td><a href='./testIndex.html'>My First Test</a></td></tr>
...

テストを実行するために、つぎのURLにブラウザーでアクセスしてください。

http://myapp.example.com/selenium/index.html

Main Test Suiteを選択し、すべてのテストを実行するボタンをクリックし、行うように伝えられたステップをブラウザーが再現する様子を観察してください。

NOTE Seleniumのテストは本当のブラウザーで動作するので、これらによってブラウザーの不一致もテストできます。1つのブラウザーでテストを作り、単独のリクエストで動作することになっているサイト上でそのほかのすべてのブラウザー上でSeleniumのテストを実施してください。

SelenimはHTMLで書かれているので、Seleniumのテストを書くことは面倒でした。しかし、FirefoxのSelenium拡張機能のおかげで(http://seleniumrecorder.mozdev.org/)、テストを実施するために必要なことはレコードセッションで1回のテストを実施するだけです。レコードセッションでナビゲートする一方で、ブラウザーのウィンドウ内で右クリックをしてポップアップメニュー内のAppend Selenium Commandのもとで適切なチェック項目を選択することで、アサート型のテストを追加できます。

アプリケーションに対してテストスイートを実施するためにテストをHTMLのファイルに保存できます。Firefoxの拡張機能によって記録したSeleniumテストも実行できるようになります。

NOTE Seleniumテストを立ち上げるまえにテストデータを再び初期化することを忘れないでください。

まとめ

自動テストとしてメソッドもしくは関数を検証するユニットテスト(unit test)と機能を検証する機能テスト(functional test)が存在します。symfonyはユニットテストのためのlimeテストフレームワークに依存し、ユニットテスト用に特化したsfTestBrowserクラスを提供します。これらのテストツールは、CSSセレクタのように、両方とも基礎から応用まで及ぶ多くのアサーションメソッドを提供します。テストを起動させるにはsymfonyのコマンドラインを使います。1つずつ実施するにはtest-unitタスクもしくはtest-functionalタスクを使い、テストハーネスを実施するにはtest-allタスクを使います。データを扱うとき、自動テストはフィクスチャ(fixture)とスタブ(stub)を使い、これはsymfonyのユニットテスト内で簡単に実現されます。

(おそらくテスト駆動開発(TDD)の方法論を利用して)アプリケーションの大部分をカバーするために十分なユニットテストをかならず書けば、内部をリファクタリングするもしくは新しい機能を追加するときに、安心感を得られドキュメントを作るための時間を節約することもできます。