復習
昨日に追加したコミュニティのタグづけ機能のおかげで、askeetのWebサイトの質問はよく編成されました。
Webアプリケーションの寿命において重要であるにもかかわらず、今まで語られなかったことがあります。ユニットテスト(単体テスト)はオブジェクト指向以降の最も偉大な進歩の一つです。ユニットテストによって安全な開発プロセスを実現し、恐れることなくリファクタリングができます。アプリケーションが何をするのかはっきりと記述することができるので、時にドキュメントに置き換えるができます。symfonyはユニットテストの推奨とサポートし、そのためのツールを提供します。 これらのツールの概要 - askeetのいくつかのユニットテストの追加 - は今日のチュートリアルの多くの時間を占めます。
Simple Test
PHPの世界ではJunitに基づくユニットテストフレームワークがたくさんあります。私たちはsymfonyのために別の物を開発しません。しかし、かわりにそれらの中でもっとも完成されているものを統合しました。Simple Testです。安定しており、十分なドキュメントが作成され、symfonyを含む、すべてのPHPプロジェクトに重要な価値を持つ多くの機能を提供します。まだ知らなければ、これらの明快で進歩的なドキュメントを見ることを強くお勧めいたします。
Simple Testはsymfonyにバンドルされていませんが、インストールは簡単です。最初にSourceForgeのPEARでインストール可能なアーカイブのSimple Testをダウンロードします。PEAR経由でインストールします:
$ pear install simpletest_1.0.0.tgz
Simple Testライブラリを使ったバッチスクリプトを書きたい場合、すべきことはスクリプトのトップにわずかなコードを挿入するだけです:
<?php require_once('simpletest/unit_tester.php'); require_once('simpletest/reporter.php'); ?>
symfonyはテストのコマンドラインを使うかどうかをしてくれます; 手短に説明します。
note
PHP 5.0.5の後方互換性がない変更のため、Simple TestはPHP 5.0.4以上の場合は現在動作しません。短く変更されるのですが、(この問題に取り組んだアルファバージョンは利用可能です)不幸なことに、このチュートリアルの残りは後のバージョンである場合はおそらく動作しません。
symfonyプロジェクトでユニットテスト
デフォルトのユニットテスト
symfonyプロジェクトはアプリケーションのサブディレクトリに分割されたtest/
ディレクトリを格納します。askeetの場合、askeet/test/functional/frontend/
ディレクトリを眺めると、すでにいくつのファイルが存在していることを確認できます:
answerActionsTest.php feedActionsTest.php mailActionsTest.php sidebarActionsTest.php userActionsTest.php
これらはすべて同じ初期コードを持ちます:
<?php class answerActionsWebBrowserTest extends UnitTestCase { private $browser = null; public function setUp () { // 新しいテストブラウザを作成する $this->browser = new sfTestBrowser(); $this->browser->initialize('hostname'); } public function tearDown () { $this->browser->shutdown(); } public function test_simple() { $url = '/answer/index'; $html = $this->browser->get($url); $this->assertWantedPattern('/answer/', $html); } } ?>
UnitTestCase
はSimple Testによるユニットテストの中心的なクラスです。setUp()
メソッドはそれぞれのテストメソッドの前で動作し、tearDown()
はそれぞれのテストメソッドの後で動作します。実際のテストメソッドは'test'という言葉で動作を始めます。コードのピースがあなたの期待通りに動作するのか、確認するには何がtrueなのか確認するメソッド呼び出しであるアサーションを使います。Simple Testにおいてアサーションはassert
で始まります。この例の場合、1つのユニットテストが実装され、モジュールのデフォルトページにおいて「user」が捜索されます。この自動生成されたファイルはあなたが始めるためのスタブです。
実際のところ、synmfony init-module
を呼び出すたびにsymfonyは作成されたモジュールに関連するユニットテストを保存するためにtest/[appname]/
ディレクトリでスケルトンを作成します。問題はデフォルトのテンプレートを修正したとたんに、スタブテストが合格しなくなることです('module $modulename'であるページのデフォルトタイトルを確認)。これらのファイルを削除し、私たち自身のテストケースを動作させます。
ユニットテストを追加する
13日目に、タグの操作に関連する2つの関数を使いTag.class.php
ファイルを作成しました。私たちのTagライブラリのためにいくつかのユニットテストを追加します。
TagTest.php
ファイルを作成します(すべてのテストケースファイルはSimple Testが見つけられるようにTest
で終わらなければなりません):
<?php require_once('Tag.class.php'); class TagTest extends UnitTestCase { public function test_normalize() { $tests = array( 'FOO' => 'foo', ' foo' => 'foo', 'foo ' => 'foo', ' foo ' => 'foo', 'foo-bar' => 'foobar', ); foreach ($tests as $tag => $normalized_tag) { $this->assertEqual($normalized_tag, Tag::normalize($tag)); } } } ?>
私たちが実装する最初のテストケースはTag::normalize()
メソッドに関係します。ユニットテストは1回に1つのケースをテストするようになっているので、私たちは期待されるテキストメソッドの結果を基本ケースに分解します。Tag::normalize()
メソッドは引数の小文字バージョンを返します。戻り値の値の前後のスペースおよび特殊文字は削除されます。5つのテストケースは$test
配列において定義され、テストするのに十分な量です。
それぞれの基本テストケースに対して->assertEqual()
メソッドを呼び出すことで正規化された入力と期待される結果を比較します。これはユニットテストの核心です。失敗したら、テストスイートが実施されたときにテストケースが出力されます。合格したテストの数がそのまま追加されます。
'FOo-bar'
で最後のテストを追加できますが、基本ケースをミックスします。このテストが失敗した場合、問題の正確な原因のはっきりしたアイディアを思いつかないでしょうし、もっと調査をする必要があります。基本ケースを使うことで、簡単に突き止められるエラーの保証をします。
note
assert
メソッドの大規模なリストはSimple Testのドキュメントで見つかります。
ユニットテストを動作させる
symfonyコマンドによって1つのコマンドですべてのテストをすぐに実施できます(プロジェクトのrootディレクトリから呼び出す):
$ symfony test-functional frontend
このコマンドを呼び出すと、すべてのtest/functional/frontend/
ディレクトリのテストが実行されます。現時点ではTagTest.php
セットだけです。これらのテストは合格しコマンドラインは次のように示します:
$ symfony test-functional frontend Test suite in (test/frontend) OK Test cases run: 1/1, Passes: 5, Failures: 0, Exceptions: 0
note
symfonyコマンドで立ち上げたテストはSimple Testライブラリを含みません(unit_tester.php
とreporter.php
は自動的に含まれます)。
反対の方法
ユニットテストの最大の利点はテスト駆動開発をしているときに経験します。この方法論では、関数を書く前にテストを書きます。
上記の例では、空のTag::Normalize()
メソッドを書き、最初のテストケース('Foo'/'foo')を書き、テストスイートを動作させます。テストは失敗します。それからTag::normalize()
メソッドにおいて、引数を小文字に変換して返すのに必要なコードを追加し、またテストを実行します。今度のテストは合格します。
空白文字のテストを追加し、実施し、失敗を確認し、空白を除去するコードを追加し、テストを再実行し、合格するのを確認します。それから、特殊文字にも同じことを行います。
テストを最初に書くことは実際に開発する前に関数がなにをすべきなのかについて焦点を当てることを手助けします。エクストリームプログラミングのような推奨される他の方法論と同じように、これもグッドプラクティスです。加えて、ユニットテストを書いていないと、後で書かないという事実を考慮に入れています。
最後のお勧めです。ここで説明したのと同じくらいユニットテストをシンプルに保つことです。テスト駆動方法論によるアプリケーションの開発では実際のコードとテストコードが同じぐらいで終わります。ですのでテストケースをデバッグするのに時間を費やしたくないことでしょう・・・
テストが失敗するとき
Tag
オブジェクトの2番目のメソッドをチェックするテストを追加します。Tag
オブジェクトは文字列で構成されるいくつかのタグをタグの配列に分割します。TagTest
クラスに次のメソッドを追加します:
public function test_splitPhrase() { $tests = array( 'foo' => array('foo'), 'foo bar' => array('foo', 'bar'), ' foo bar ' => array('foo', 'bar'), '"foo bar" askeet' => array('foo bar', 'askeet'), "'foo bar' askeet" => array('foo bar', 'askeet'), ); foreach ($tests as $tag => $tags) { $this->assertEqual($tags, Tag::splitPhrase($tag)); } }
note
グッドプラクティスとして、テストを行うクラスからテストファイルを名づけ、これからテストしようとしているメソッドからテストケースを名づけることをお勧めします。test/
ディレクトリにはすぐに多くのファイルが収納され、長期的には、テストを見つけるのが難しくなるからです。
テストを再実行しようとすると、失敗します:
$ symfony test-functional frontend Test suite in (test/frontend) 1) Equal expectation fails as key list [0, 1] does not match key list [0, 1, 2] at line [35] in test_splitPhrase in TagTest in /home/production/askeet/test/functional/frontend/TagTest.php FAILURES!!! Test cases run: 1/1, Passes: 9, Failures: 1, Exceptions: 0
大丈夫です。test_splitPhrase
のテストケースの1つが失敗しました。どれなのかを見つけるために、テストが合格したときに一度除去する必要があります。今回、シングルクォートの処理をテストするとき、最後のテストです。現在のTag::splitPhrase()
メソッドはこの文字を適切に変換しません。宿題の一部として、明日、訂正をする必要があります。
配列にあなたが多くの基本テストケースを積み上げすぎた場合、失敗を見つけるのは難しいことは明らかです。Simple Testはテストがどこで失敗したのかメソッド名を記載するので、長いテストケースをメソッドに分割にするのは常に望ましいです。
Webブラウザセッションをシミュレートする
Webアプリケーションは多かれ少なかれ関数のように動作するオブジェクトのすべてではありません。複雑なページリクエストのメカニズム、HTMLの出力結果とブラウザとの情報のやりとりはsymfony製のWebアプリケーションのための完全なユニットテストのセットを構築するまえに、より多くのことに直面します。
シンプルなWebアプリケーションのテストを実装するために3つの異なる方法を検査します。テストは最初の質問の詳細をリクエストしなければならないのと、回答のテキストが存在することを前提としています。このテストをaskeet/test/functional/frontend/
ディレクトリのQuestionTest.php
ファイルに設置します。
sfTestBrowser
オブジェクト
symfonyはsfTestBrowser
と呼ばれるオブジェクトを用意しています。これはブラウザをシミュレートし、もっと重要なことは、Webサーバー無しで使えることです。フレームワーク内部にあることで、このオブジェクトがhttp転送レイヤーを完全に回避できます。このことはsfTestBrowser
によってシミュレートされたブラウザは速く、サーバーを使用しないのでサーバーの設定から独立しています。
このオブジェクトでページをどのようにリクエストするのか見てみましょう:
$browser = new sfTestBrowser(); $browser->initialize(); $html = $browser->get('uri'); // $html上でいくつかのテストを行う $browser->shutdown();
get()
リクエストはパラメータ(内部のURIではなく)として、ルーティングされたURIを取得し、生のHTMLページ(文字列)を返します。UnitTestCase
オブジェクトのassert*()
メソッドを使用して、このページについてあらゆる種類のテストを続行できます。
URLバーをブラウジングするものとして、メソッド呼び出しに引数を渡すことができます:
$html = $browser->get('/frontend_test.php/question/what-can-i-offer-to-my-stepmother');
特別なフロントコントローラ(frontend_test.php
)を使う理由は次のセクションで説明されます。
sfTestBrowser
はCookieをシミュレートします。単独のsfTestBrowser
オブジェクトで、次から次へ複数のページをリクエストすることが可能で、フレームワークによって単独のセッションの一部とみなされます。加えて、sfTestBrowser
は内部URLの代わりにルーティングされたURIを使用するという事実はルーティングエンジンをテストするのが可能であることを意味します。
Webテストを実装するために、test_QuestionShow()
メソッドを次のように構築します:
<?php class QuestionTest extends UnitTestCase { public function test_QuestionShow() { $browser = new sfTestBrowser(); $browser->initialize(); $html = $browser->get('frontend_test.php/question/what-can-i-offer-to-my-step-mother'); $this->assertWantedPattern('/My stepmother has everything a stepmother is usually offered/', $html); $browser->shutdown(); } }
すべてのWebユニットテストは新しいsfTestBrowser
を必要とします。コードの一部分を->setUp()
メソッドと->tearDown()
メソッドに移動させた方がよりベターです:
<?php class QuestionTest extends UnitTestCase { private $browser = null; public function setUp() { $this->browser = new sfTestBrowser(); $this->browser->initialize(); } public function tearDown() { $this->browser->shutdown(); } public function test_QuestionShow() { $html = $this->browser->get('frontend_test.php/question/what-can-i-offer-to-my-step-mother'); $this->assertWantedPattern('/My stepmother has everything a stepmother is usually offered/', $html); } }
追加したすべての新しいtest
メソッドはクリーンなsfTestBrowser
オブジェクトを保有します。このチュートリアルの最初で触れた自動生成テストケースを思い出しているかもしれません。
WebTestCaseオブジェクト
Simple Testは、ナビゲーション、内容とCookieのチェックとフォームハンドリングのための機能を含むWebTestCase
クラスを搭載しています。このクラスを継承するテストによってhttp転送レイヤーによるブラウジングセッションをシミュレートできます。繰り返しますが、Simple Testのドキュメントでこのクラスの使い方が詳しく説明されています。
Webサーバーはすべてのリクエストの中心にあるので、WebTestCase
によって構築されたテストはsfTestBrwoser
で構築されたものよりも遅いです。Webサーバーの構成を動作させることも必要です。しかしながら、WebTestCase
オブジェクトはassert*()
メソッドに加えて数多くのナビゲーションメソッドが付属しています。これらのメソッドを使うことで、複雑なブラウジングセッションをシミュレートすることが可能です。WebTestCase
ナビゲーションメソッドのサブセットは次の通りです:
- | - | - |
---|---|---|
get($url, $parameters) |
setField($name, $value) |
authenticate($name, $password) |
post($url, $parameters) |
clickSubmit($label) |
restart() |
back() |
clickImage($label, $x, $y) |
getCookie($name) |
forward() |
clickLink($label, $index) |
ageCookies($interval) |
私たちはWebTestCase
によって以前と同じテストケースを容易に実施できます。Webサーバーがリクエストするので、完全なURIを入力する必要があります:
require_once('simpletest/web_tester.php'); class QuestionTest extends WebTestCase { public function test_QuestionShow() { $this->get('http://askeet/frontend_test.php/question/what-can-i-offer-to-my-step-mother'); $this->assertWantedPattern('/My stepmother has everything a stepmother is usually offered/'); } }
このオブジェクトの追加メソッドは投稿フォームが処理される方法のテストを手助けしてくれます。たとえば、ログインプロセスのユニットテストです:
public function test_QuestionAdd() { $this->get('http://askeet/frontend_dev.php/'); $this->assertLink('sign in/register'); $this->clickLink('sign in/register'); $this->assertWantedPattern('/nickname:/'); $this->setField('nickname', 'fabpot'); $this->setField('password', 'symfony'); $this->clickSubmit('sign in'); $this->assertWantedPattern('/fabpot profile/'); }
手動でフィールドに値を設定しやすくなります。POST
リクエストを行うことでシミュレートをしなければならない場合(->post($uri, $parameters)
を呼び出すことで可能です)、多くの実装方法に依存しているので、テスト関数、アクションのターゲットとアクションのターゲットに書く必要があります。Simple Testによるフォームテストの詳しい情報はSimple Testのドキュメントの関連する章を読んでください。
Selenium
sfTestBrowser
とWebTestCase
テストの大きな欠点はJavaScriptをシミュレートできないことです。とても複雑なインタラクション、たとえばAJAXインタラクションのために、ユーザーが行うマウスとキーボードを正確に再現できることが必要です。通常は、これらのテストは手動で再現されますが、とても時間がかかり、失敗しがちです。
今回の解決方法はJavaScriptの世界からやってきます。Seleniumと呼ばれ、Selenium Recorder extension for Firefoxが採用するとより良いでしょう。Seleniumは現在のブラウザウィンドウを使用し通常のユーザーの世界と同じようにページのアクションのセットを実行します。
Seleniumはsymfonyにデフォルトで搭載されていません。インストールするには、web/
ディレクトリで新しいselenium/
ディレクトリを作成する必要があります。そして、そこでSeleniumアーカイブを解凍します。SeleniumがJavaScriptに依存するためで、たいていのブラウザの標準のセキュリティ設定ではアプリケーションが同じホストとポートで利用可能ではない限り、実行できません。
note
外部からアクセスできるので、本番用ホストにslenium/
ディレクトリを転送しないように注意してください。
SeleniumテストはHTMLで書かれ、selenium/tests/
ディレクトリに保存されます。たとえば、質問の詳細に関するシンプルなユニットテストを実行するには、testQuestion.html
という名前のファイルを作成します:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <meta content="text/html; charset=UTF-8" http-equiv="content-type"> <title>Question tests</title> </head> <body> <table cellspacing="0"> <tbody> <tr><td colspan="3">First step</td></tr> <tr> <td>open</td> <td>/frontend_test.php/</td> <td> </td> </tr> <tr> <td>clickAndWait</td> <td>link=What can I offer to my step mother?</td> <td> </td> </tr> <tr> <td>assertTextPresent</td> <td>My stepmother has everything a stepmother is usually offered</td> <td> </td> </tr> </tbody> </table> </body> </html>
テストケースは3つのカラムのテーブルを含むHTMLドキュメントによって表現されます: コマンド、ターゲット、値です。しかしながら、すべてのコマンドは値を取得しません。このケースの場合、カラムを空白しておくか、テーブルをより見やすくするために
を使います。
同じディレクトリに設置されたTestSuite.html
ファイルのテーブルに新しい行を挿入することで、グローバルテストスイートにこのテストを追加する必要もあります:
...
<tr><td><a href='./testQuestion.html'>My First Test</a></td></tr>
...
テストを実行するには、次のURLにブラウザでアクセスします。
http://askeet/selenium/index.html
すべてのテストを実施するためにボタンをクリックするよりも、'Main Test Suite'を選択して、指定したステップを再現されるのかブラウザで確認してください。1つのブラウザでテストを構築して、1つのリクエストでサイトが動作するのかテストします。
note
Seleniumテストは実際のブラウザで実行できるので、ブラウザの不整合テストを可能にします。1つのブラウザでテストを構築し、1つのリクエストでサイトが動作するのかをテストします。
SeleniumテストがHTMLで書かれているという事実はSeleniumテストを書くことをやっかいにすることもあります。しかし、Firefox Selenium拡張機能のおかげで、記録されたセッションでテストを1回実行するために、1つのテストを作成するだけで済みます。記録セッションのナビゲーションにおいて、ブラウザのウィンドウ内で右クリックすること、およびポップアップメニューにあるAppend Selenium Commandの元で適切なチェックを選択することで、アサート型のテストを追加できます。
たとえば、次のSeleniumテストはAJAXの質問レーティングをチェックします。ユーザーの'fabpot'はログインし、これまでのところ興味のないページだけにアクセスするために質問の2番目のページを表示し、それから'interested?'リンクをクリックし、'?'を'!'に変更します。Firefoxの拡張機能によってすべて記録され、30秒もかかりません:
<html> <head><title>New Test</title></head> <body> <table cellpadding="1" cellspacing="1" border="1"> <thead> <tr><td rowspan="1" colspan="3">New Test</td></tr> </thead><tbody> <tr> <td>open</td> <td>/frontend_dev.php/</td> <td></td> </tr> <tr> <td>clickAndWait</td> <td>link=sign in/register</td> <td></td> </tr> <tr> <td>type</td> <td>//div/input[@value="" and @id="nickname" and @name="nickname"]</td> <td>fabpot</td> </tr> <tr> <td>type</td> <td>//div/input[@value="" and @id="password" and @name="password"]</td> <td>symfony</td> </tr> <tr> <td>clickAndWait</td> <td>//input[@type='submit' and @value='sign in']</td> <td></td> </tr> <tr> <td>clickAndWait</td> <td>link=2</td> <td></td> </tr> <tr> <td>click</td> <td>link=interested?</td> <td></td> </tr> <tr> <td>pause</td> <td>3000</td> <td></td> </tr> <tr> <td>verifyTextPresent</td> <td>interested!</td> <td></td> </tr> <tr> <td>clickAndWait</td> <td>link=sign out</td> <td></td> </tr> </tbody></table> </body> </html>
Seleniumテストを立ち上げる前に、テストデータを再初期化することを忘れないでください(batch/load_data.php
を呼び出します)。
note
Seleniumが動作しないので、AJAXリンクをクリックした後で手動で停止アクションを追加しなければなりませんでした。これがSeleniumでAJAXインタラクションをテストするための一般的なアドバイスです。
アプリケーションのテストスイートを構築するためにHTMLのテストファイルを保存できます。Firefoxの拡張機能は記録したSeleniumテストを実行することもできます。
環境についていくつかの説明
Webテストにはフロントコントローラを使わなければなりません。同様に特別な環境(すなわち設定)を使うことができます。symfonyはデフォルトですべてのアプリケーションのためのテスト環境を提供します。とりわけ、ユニットテストがあります。アプリケーションのconfig/
ディレクトリでカスタム設定の一式を定義できます。デフォルトの設定パラメータは次の通りです(askeet/apps/frontend/config/settings.yml
から抜粋):
test: .settings: # E_ALL | E_STRICT & ~E_NOTICE = 2047 error_reporting: 2047 cache: off stats: off web_debug: off
cache
、stats
とweb_debug
ツールバーはオフに設定されています。しかしながら、まだコードの実行はログファイルにトレースされています(askeet/log/frontend_test.log
)。特定のデータベースの接続設定を使用できます。たとえばテストの日付で他のデータベースを使用するためなどです。
上述で言及したすべての外部URIがfrontend_test.php
を表示するのはそういうわけです。test
フロントコントローラを指定しなければなりません。さもなければ、デフォルトで本番用のindex.php
コントローラが使われます。そして、異なるデータベースを使用する、またはユニットテストのための個別のログを用意できません。
note
本番環境ではWebテストは起動しません。これらは開発ツールで、そういうものとして、ホストサーバーではなく開発者のコンピュータで動作しなければなりません。
それではまた明日
symfonyで構築されたPHPアプリケーションをユニットテストする完璧なソリューションは存在しません。今日のそれぞれの3つのソリューションは大きな利点を持ちますが、広範囲におよぶユニットテストの方法論があるとしたら、おそらくそれら3つすべてが必要になるでしょう。askeetに関しては、ユニットテストはSVNソースに少しづつ追加されます。定期的に確認するか、アプリケーションの堅牢性を強化するためにあなた自身のものを提案してください。
ユニットテストは回帰現象を避けるためにも使われます。メソッドをリファクタリングすることは以前現れなかった新たなバグを作り出します。本番環境においてアプリケーションの新しいリリースをデプロイする前にすべてのユニットテストを実行する理由です。このテストのことを回帰テストと呼ばれます。アプリケーションの開発を取り扱うときにおいて、もう少し話します。
明日は...、明日は明日の風が吹きます。今日のチュートリアルの質問がありましたら、askeetフォーラムで質問してください。
This work is licensed under the Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License license.