書いた人: moriq
これまでの連載では Scaffold で遊んできましたが、今日はテストをしてみましょう。
読者の皆さんはもうすでに Rails を使っていくつかかっこいいアプリケーションを作られていることと思います。 では、そのアプリケーションをテストしてみましたか。ここでいうテストとは、ブラウザを立ち上げて行うテストではなく、test/ にコードを書いて行うテストです。 まだ書いていない? それは好都合ですね。
もう書いたよ、なんたってテスト駆動開発だからね! すばらしい。私も見習いたいものです。では、そのテストはプログラムをどの程度カバーできていますか。 Scaffold はテストも生成してくれますが、そのテストには明らかに抜けがあります (少なくとも Rails 1.0 には)。どこだか分かりますか。
そんなテストの話をしたいと思います。
ここでとりあげるのは、ソフトウェアテストのうち、ユニットテスト (unit test) と機能テスト (functional test) といわれるものです。 まず、言葉の意味を確認しておきましょう。
ユニットテストはユニットを対象とするテストです。 ユニットとは、テスト可能なコードの固まりで、それ以上分割できないものです。 なんだか定義が循環していますが、公開されたインターフェイスが見えているブラックボックスなモジュール1個をユニットというのだ、としておきます。
ユニットテストは単体テストともいいます。 ‘実践的プログラムテスト入門’によれば、ユニットテストのテスト対象であるユニットとは、テスト可能な最小の単位であり、ユニットテストの対象には別のユニットを含めません。 相手がいないと動作しない場合は、結合するユニットをシミュレートするために、ドライバとスタブ1をつなげることになります。 ほかのユニットとつながった状態で行うテストは、コンポーネントテストといいます。
定義はこうなのですが、実際にはコンポーネントテストもユニットテストといっていることがままあります。 例えば今回採り上げる Rails の unit test は、テストの実行時に Active Record 経由でデータベースにアクセスしたり、外部ライブラリをスタブやモックで置き換えずに直接呼び出したりしますから、定義上はコンポーネントテストです。
Active Record は、テストをクリアした信頼できるコンポーネントですから、これにつないでテストしても、バグの所在は明らかです (ええ、明らかですとも)。 仕様が明確で信頼できるフレームワークに接続した状態のコンポーネントテスト、これを広義のユニットテストといっても問題はないでしょう。
Rails での unit test は、モデルに対するテストです。
機能テストは、動作テストともいいます。あるいは受け入れテストともいいます。 機能テストのテスト対象は、システムの振る舞いです。
Rails での functional test は、あるパラメータを伴ってコントローラにアクセスしたときに、想定される結果を得られるかというテストです。モデルのデータ・ページの要素・リダイレクト先といったことをテストします。
Rails でのテストについて見ていく前に、Ruby のユニットテストライブラリである Test::Unit の使い方を確認しておきましょう。 Rails でのテストも Test::Unit を使います。
Test::Unit は、その名の通り、ユニットテストを行うためのライブラリです。
簡単に使い方を説明します。 まず、Test::Unit::TestCase を継承したクラスを用意し、そこに名前が test_ で始まるメソッドを定義します。 それから、アサート (assert) というテスト用のメソッドを使ってテストを書きます。
これを t3.rb という名前で保存して実行すると
こうなります。 assert メソッドは第一引数が true であることを期待します。 ここでは期待を裏切り false を与えていますから (1 == 2 を評価すると false になりますね) test_foo の結果は F (Failure) です。 assert メソッドの第二引数は、期待が裏切られたときに出力するメッセージです。
Test::Unit の詳しい使い方については、次のページを参照してください。
Test::Unit を使うと、ある入力が期待通りにある出力を返すかどうかということを確認できます。 重要なのは、この確認を繰り返し実行できることです。 ソフトウェアは作っていくうちにさまざまな副作用をもたらします。 これから行う修正が以前の正しい動作を壊してしまう心配は常にあります。 このときテストがあらかじめ用意されていれば、その修正が、少なくともテストに書いた動作には影響がないことを確認できます。
修正による新たなバグをチェックするテスト、これを回帰テスト (regression test; 退行テスト) といいます。
ほかの言語のユニットテストの枠組みと比べて、Ruby の Test::Unit が優れているのは、テストを書いたらそれをすぐ実行できることだと思います。 Ruby の Test::Unit には、クラスを定義するだけで実行できるようにするための、興味深い仕掛けがあります。
Test::Unit で作ったテストスクリプトに -h を付ければヘルプが出力され、コマンドラインオプションを確認できます。
いくつかあるオプションのうち、実行するテストを絞り込むのに使えるのが -n と -t です。
このように // で囲むと正規表現で指定できます。 テストを書きながら開発するときには、これらをうまく使って、すばやくテストを行うようにしましょう。
Rails でのテストでも test/unit を使いますが、Rails アプリケーションをテストしやすいように拡張されています。
Rails でのテストについての詳細なドキュメントが次のところにあります (英語)。
フィクスチャとは、一般的には、テストに使う初期データのことをいいます。 assert で使う前に setup やテストメソッドで生成するオブジェクトがフィクスチャです。 ただ、Rails では、Active Record でテストデータベースを設定する機能を特に Fixture と呼んでいることから、 データベースまわりの少し狭い意味でフィクスチャと言っていることが多いようです。
フィクスチャを使う際に、特に注意しなければならないのは、association です。 テスト対象のモデルと association でつながっているモデルのフィクスチャを読み込むように指定しておかないと、予期しない現象に見舞われることがあります。
また、これは特に MySQL で MyISAM エンジンを使っている場合に当てはまりますが、トランザクションに対応していないデータベースを用いるときには、
この指定を忘れないようにする必要があります。そうしないとテストの teardown でデータが元に戻らず、予期しない現象に見舞われることになります。
Rails のテストをまとめて実行するときには、rake タスクを実行します。
プラグインのテストはアプリケーションのテストとは別に実行するようになっています。
ソフトウェアテストの基本的な考え方を紹介します。
テストを設計する際に決め手となるのは、入力と結果の組み合わせです。 この組み合わせをきちんと定義できれば、テストに書くことはおのずと定まります。
よく使われるブラックボックステストの手法をまとめておきます。
ある入力において、結果が同じ、あるいは処理が同じになる値は、テストの上では同値とみなせます。 入力条件ごとに有効同値 (条件を満たす値) と無効同値 (条件を満たさない値) を決めることができれば、その値の組み合わせだけテストすればよいことになります。
とはいうものの、‘はじめて学ぶソフトウェアのテスト技法’によれば、これは誰でも無意識のうちに使っている手法なのだそうです。
同値クラステストから導かれる手法として、境界値テストがあります。
バグは境界に現れます。要件があいまいになるのはたいてい境界条件ですし、実装を誤るのは分岐条件や範囲の端っこでのことが多いからです。 入力条件の境界値と隣りの値を調べればバグを見つけやすくなります。
関連した 2 変数以上の境界値テストはドメインテスト (domain testing) として理論化されています。 境界値テストより少ない組み合わせで効率的にテストを実施できます。
Rails のテストでは入力と結果をどのようにテストに落とし込んでいけばいいのでしょうか。
まず、モデルに対するユニットテストについては、Rails 固有の HowTo といったものは特になく、一般的な xUnit2 の知識がそのまま使えます。
しいていえば、 Active Record の仕組みを知っておくと、テストを書くとき役立ちます。 例えば、 Active Record はフェッチした値をキャッシュするので、reload (あるいは refresh) しないと、思い通りの結果を得られないことがあります。
効率良くテストを行うにはどのような方針で初期データ (フィクスチャ) を選択すればよいのでしょうか。
入力データとしてさまざまな組み合わせを試さないといけませんが、闇雲に値を選択しても効果が上がりません。 効率的にバグをあぶり出すためには、同値クラス・境界値・ドメインといった、ブラックボックステストの手法を元にするとよいでしょう。
現実に即した値は代表値として使えます。現実的なデータを元にフィクスチャを用意するとよいでしょう。 このフィクスチャを元に、項目をひとつづつ無効値にずらしてやれば、同値クラステストを効率良く進めることができます。
現実的な値とは別に、境界値をカバーする「極端な」フィクスチャを用意しておくと、境界値テストを効率良く進めることができます。
ユニットテストを行って、モデルの動作を確認できたら、コントローラとビューに対する機能テストに進むことができます。
ほかの開発環境では、開発のはじめの段階では、機能テストまではなかなか手が回らないと思いますが、 Rails では開発者が自らの手で機能テストを気楽に書くことができます。
Rails の機能テストを書く際に知っておく必要があるのは、 入力であるブラウザからの操作をどのように記述すればいいのか、 また、期待される結果であるレスポンスをどのように記述すればいいのか、ということです。
機能テストを書くときは、ブラウザからパラメータがどのような形で渡るのかを把握しておく必要があります。
把握できたら、get, post メソッドを使って、リクエストをシミュレートします。
また、セッションについては、@request.session に直接指定します。
ブラウザから渡されるパラメータを調べる前に、Rails のパラメータ params が受け取りうる値について指摘しておきます。
Web アプリケーションへの接続は、そのアプリケーションが用意したページに限定されません。ブラウザに限ったことでもありません。 クライアントを自分で書けば、アプリケーションに渡すパラメータはサーバが許す限り何でもありです。 さらに、Rails アプリケーションは XML, YAML 形式のパラメータを受け取ることができ、これらは Ruby オブジェクトに変換されて評価されます。 つまり、Rails アプリケーションが解釈できるオブジェクトなら、何でもパラメータとして渡すことができるのです。 セキュリティを考える際には、このことを念頭に置く必要があります。
未入力フィールドが渡す値を調べておきましょう。 ブラウザからのアクセスを前提にした話です。
テキストフィールドに何も入力せずに submit するとパラメータの値は空文字列 “” になります。
チェックボックスにチェックを付けずに submit すると、パラメータには何も渡りません。値だけでなくそもそもキーが存在しないことになります。
そのまま submit すると
チェックを付け替えてみると
デフォルトのチェックの状態に関係なく、チェックを付けたキーだけが現れます。
ただし、ヘルパメソッド check_box を使うと、チェックボックスと共に隠しフィールドが用意されるので、チェックボックスの処理を特別扱いしなくてよくなります。
これは次のように展開されます。
ただし上で見たように、タグヘルパメソッド check_box_tag を使うときは、隠しフィールドは付きません。
ラジオボタンもチェックボックスと同じです。
これは次のように展開されます。
チェックを付けないと
チェックを付けると
ラジオボタンをグループのうちでひとつも選択せずに submit すると、パラメータには何も渡りません。
ブラウザからアクセスすると、select タグの選択肢は必ずどれかひとつ選択した状態になっています。 ヘルパメソッド select で :include_blank => true を指定したときは未選択だと “” が渡ります。
日付や時刻を扱うときは、date_select, time_select, datetime_select といったヘルパメソッドで select タグの組を生成します。
date_select であればデフォルト値は今日の日付になります。
include_blank => true を指定したときは “” が渡ります。
日付や時刻を扱う際にはこのように奇妙な名前を持つ複数のパラメータが渡ります。 テストを書くときも、基本的にこれに合わせてパラメータを書かないといけません。
ただし、場合によっては直接 Date, Time オブジェクトを渡してもかまいません。
これは、アクションでパラメータがどのように扱われるかによって変わってきます。 しかし、このような書き方はあまりお勧めできません。 このテストに合わせて params[:pet][:birth] を使って実装してしまうかもしれません。 そうしてテストに成功したとしても、実際にブラウザからアクセスしたときには、そのようなパラメータは現れませんから、きっとエラーになってしまうでしょう。 機能テストの書き方としては間違っていると思います。
Rails でファイルをアップロードするときのビューは、次のようになります。
ここで問題になるのは、ファイルを参照せずに submit したとき、file に何が渡るかということです。 アップロード先のビューに次のデバッグコードを仕掛けてみると、
このようになり、ファイルを参照しないときは params[:file] に StringIO オブジェクトが設定されることが分かります。 この StringIO オブジェクトには特異メソッド original_filename, content_type が定義されており、それぞれ値は “” になっています。これをチェックに用いれば、アップロードされていない場合に対応できます。
さて、これを機能テストとして書くにはどうすればいいでしょう。 StringIO オブジェクトに特異メソッド定義をすれば Mock が作成できます。3
結果を確認するために、専用のアサートメソッドを使います。
assert_response を使ってステータスコードを確認できます。
success のときはレンダリングしたテンプレート名を assert_template で確認できます。
また、テンプレートに渡されたインスタンス変数の値を assigns で確認できます。
session, flash の値はそれぞれ session, flash で確認できます。
また、テンプレートを元に出力された HTML の要素を assert_tag で確認できます。
redirect のときはリダイレクト先を assert_redirected_to で確認できます。
リダイレクト先まで確認したいときは follow_redirect を使ってリダイレクトをシミュレートできます。
アサートを書くとき、パラメータの型が問題になることがあります。
具体的な例として、params[:id] の値を redirect_to の :id パラメータに指定することを考えてみましょう。 post に渡したパラメータを redirect のパラメータとしてそのまま渡してみます。
何の問題もないように見えますが、このテストは失敗します。 params[:id] は文字列 “1” になるからです。 よって、パラメータを整数に変換する必要があります。
あるいはテスト側 assert_redirected_to のパラメータを文字列に変更します。 この場合はpostに渡すパラメータも文字列にそろえたほうが分かりやすいです。
結局、整数・文字列どちらがいいのでしょうか。
ブラウザから送られるクエリは、Railsによって文字列要素を持つハッシュに変換されます (正確に言うと、このハッシュは入れ子になることがありますし、要素が配列になることもあります。どちらにしても末端の要素は文字列です) 。実際の動作に合わせる意味では、テストでも文字列のハッシュを使うほうが良いと言えます。
しかし実際には、数値にしたほうが良いことも多いでしょう。例えば、次の実装を見てみましょう。
ここでいちいち .id を付けてから .to_s したり “#{}” で囲ったりするのはださいですよね。 これに対応するテストをパスさせるには、パラメータを文字列ではなく数値にする必要があります。
また、パラメータをそのまま評価すると何かと危険です。整数であるべきパラメータなら整数に変換してから使う、こうすれば、パラメータにおかしな値を渡す攻撃からシステムを守ることができます。
外部の環境に依存するテストは書きにくいものです。 典型的な問題として、時刻を扱うテストがあります。 時刻に対するテストを行うためには、どのように設計すればよいのか考えてみます。
ひとつの解法としては、フィクスチャのほうを動的に設定することが考えられます。 AWDwR にもこの解法の例が載っています。
ここでは生年月日と年齢の関係を例に採ります。 今日が20歳の誕生日である顧客を用意する必要があれば、次のように書けます。
今日の日付によって処理が変わることがあります。 特に月 (month) や日 (day) の値が重要な役割を担うときは、先に紹介した動的なフィクスチャではうまくいきません。 例えば、25日締めで年齢計算は翌月1日時点で行うことを考えてみてください。 基点となる日付の計算が必要になります。 そして計算式 (ロジック) がテスト側にあるのは本末転倒ですよね。
フィクスチャの誕生日を固定して、計算して得られる年齢をテストすることを考えてみましょう。
計算を行うメソッドに、基点となる時刻を与えられるようにすれば、任意の基点についてテストできるようになります。
また、Ruby の動的な力を利用して、テストのときだけ Time.now を再定義する方法もあります。
こうすれば、今日の日付を 2006-02-25 とみなしてテストを考えればよいことになります。
コントローラで設定したモデルの配列 @pets が空であるかを調べるときは、先に紹介したように、 テンプレートに渡されたインスタンス変数の値を確認できる assigns を使って、
このように書けます。
あるいは配列の要素が 1 つのとき
これは問題ありませんね。
配列が要素を複数持つときには、いくつか問題があります。 ひとつめは、アサートの結果が分かりづらくなることです。
モデル (の配列) を assert の期待値にすると、inspect (pp) の出力にオブジェクトの持つ属性値の全てが含まれてしまいます。
わかりやすい属性値、例えば name に map しておくと、テストに失敗したとき見やすくなります。
ただし、フィクスチャの name の値をそのまま期待値に書くと DRY に反するので (後述)、フィクスチャ名で指定すべきです。
問題点のふたつめは、配列要素の並び順です。
配列の大きさが 1 より大きいときに配列要素をチェックするには、 要素が並び替えられていることを実装の責任とする (つまり仕様に盛り込む) か、 順序は自由であるとし、テスト側で並び替えてからチェックする必要があります。
list アクションや検索結果を出力するアクションでは、得られた配列要素の順序が偶然のものなのか、それとも並び替えによる必然のものなのか、注意する必要があります。 データベースの仕様によっては、order by 句のない select の結果は primary key である id の順序で並んでいることを期待できますが、この振る舞いに依存しないほうがよいでしょう。 運用し始めは問題なかったのに、途中の行が削除されて間に挿入されたために、並び順に問題が生じたことがありました。 並び替えのし忘れは、結構やってしまいがちだと思うのですが、どうでしょう。
この暗黙の順序に依存しないように、フィクスチャに設定する id の順序を、あえて期待される順序とは異なるように入れ替えておくのも、良い対策だと思います。
テストを書くときでも DRY 原則は有効です。同じことは書かない。この約束を守るなら、テストはどう書くことになるでしょうか。
まず、前提条件としてのフィクスチャに注目しましょう。 フィクスチャは入力データであり、テストを書くときの基礎になります。 DRY 原則を守るなら、フィクスチャに書いたことは、ほかのところで書いてはいけない、といえます。
具体的には、例えば Customer モデル c の c.name をチェックするとします。 フィクスチャに
とあり、
このように期待値として文字列 ‘tom’ を書くと、後でフィクスチャ上の値を変更したときにこちらも変更しなくてはいけません。
このようにフィクスチャの値を参照するように書くほうがよいことになります。
id についても同じことがいえます。 以前「パラメータの型」で次のような例を示しました。
これは scaffold のテストでも見られる典型的なテストで、:id には数値を指定します。しかし、これは次のように書いたほうがよりよいように思います。
フィクスチャにおける DRY 原則のメリット:
DRY 原則を守るため、あるいは見通しをよくするため、いくつかのアサートをひとつのメソッドに括り出すことができます。
以前「モデルの配列に対するテスト」で、ペットの配列 @pets に対するアサートを取り上げました。これを何度も使うことになるなら、次のように括り出せます。
ちなみにこの pet 達は MySQL リファレンスマニュアルの pet データ を元にしています。
括り出したメソッドの名前は test_ で始まらないようにします。そうしないと、テストメソッドとして呼ばれてしまいます。
アサートをまとめるときに注意したいのは、結果が同じ、あるいはテスト対象での処理が同じになるアサートを何度も繰り返す必要はないということです。あるテストメソッドを元に、条件となるパラメータを少し変えて別のテストメソッドを用意したとします。その条件の変更に影響しないアサートが含まれていれば、それは削除すればよく、括り出す必要はありません。
テスト A が成功するならテスト B は必ず成功する。このとき、テスト B は削除してかまわない、といえます。
例として、scaffold で生成される次のテストを評価してみましょう。
assert_redirected_to はリダイレクトのパラメータが期待するものと一致するかをチェックします。リダイレクトすることが前提になりますから、これが成功するなら assert_response :redirect は必ず成功します。つまり、テスト A はテスト B を含むといえますから、テスト B は削除してもテストの強度は変わりません。
重複を取り除くメリット:
デメリット:
これはつまり、同じことを何度もテストしないということです。 テストは網羅できませんが、効率のよいテストを考えることはできます。 良いテストとは、バグを見つけるテストです。 バグを見つける効率を高めるために、異なるバグを見つけられるテストの組を設計すべきです。
先の 2 月生まれのペットの検索に加えて、8 月生まれのペットの検索を書いたとします。
これらは確かに結果が異なっていますが、処理の面では同等です。 2 月で成功するのに 8 月で失敗する理由はなさそうです。 month=1..12 の範囲は、テストを行う上では同値であるとして差し支えありません。 ですから、あえて 8 月の検索を書く必要はないのです。
これに対して、month=’ ‘ のときは処理が異なるので (書き忘れていましたが、空白ならその項目を検索条件に使わないという要件になっています) テストを書く必要があります。
リファクタリングの方法は普通のコードと同じです (たぶん)。 ただ、テストのテストは普通書きませんから、テストのコードは見通しがよくないといけません。 こてこてに構造化して、何をアサートしているのか一目で分からなくなるのはよくないです。
テストメソッドの粒度、テストクラスの粒度はどのように考えればよいのでしょうか。
用意されるテストの雛形に引きずられてしまいがちですが、 ひとつのクラスに対するテストクラスはひとつであると決まっているわけではありません。 むしろ、テストすることひとつに対してテストクラスを作ると、テストすることが明確になり、テストメソッド名が短くてすみます。 テストメソッド名に同じプレフィックスを持つものがたくさんできてきたら、クラスの分割を検討すべきです。
ここまでブラックボックステストの考え方に倣って、効率の良いテストの書き方を考えてきましたが、 ソフトウェアテストにはもうひとつ、ホワイトボックステストという考え方があります。 こちらはコードを解析して経路を網羅しようという、パス網羅という考え方が基本にあります。
網羅のことをカバレッジ (coverage) というのですが、書いたテストがどの程度コードを網羅しているのか調べるためのツールが作られています。それがカバレッジツールです。
ここではいくつかある Ruby/Rails 用のカバレッジツールを紹介します。 いずれも行指向のカバレッジで、条件網羅の面から見ると弱いのですが、テストを書くときの目安として役立ちます。
rcov は Mauricio Fernandez さん作の Ruby 用のカバレッジツールです。
rcov は RPA (Ruby Production Archive) のパッケージとして配布されていますので、先に rpa-base をインストールする必要があります。rpa-base をインストールすると rpa コマンドが使えるようになります。 rcov のインストールは
で行えます。 なお、最新版が eigenclass - Better code coverage for Ruby: rcov 0.1.0 prerelease で配布されているようです。こちらは rpa-base をインストールしなくてもダウンロードできます。
rcov コマンドがインストールされます。 拡張ライブラリを make できる環境では rcovrt.so もインストールされます。 set_trace_func の替わりに Cレベルで rb_add_event_hook を行い、高速に動作するようです。
Test::Unit で記述した任意のテストスクリプトを引数にして rcov コマンドを実行します。 すると coverage/ ディレクトリに結果が出力されます。
insurance は Lunchbox Software さん作の Ruby/Rails 用のカバレッジツールです。
railscov コマンドと拡張ライブラリ insurance_tracer.so がインストールされます。
Rails アプリケーションのディレクトリに rake タスクをインストールします。
rake タスクを実行します。 すると insurance/ ディレクトリに結果が出力されます。
もりきゅうは ミッタシステム のプログラマです。
著者の連絡先は moriq@moriq.com です。
ドライバは上位モジュールの代替プログラム、スタブは下位モジュールの代替プログラムです。Rails のテストでは、テストの実行環境を用意する test_help.rb がドライバ、ActiveRecordの test 環境や TestRequest, TestResponse がスタブといえそうです。 ↩
テスティングフレームワークの総称。各言語用に開発されています。 ↩
A Guide to Testing the Rails: 9. Testing Your Controllers の “9.7 Testing File Uploads” より ↩