RubyOnRails を使ってみる 【第 6 回】 テストの書き方

書いた人: moriq

Ruby on Rails - テストの前のひとこと

これまでの連載では 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 は、あるパラメータを伴ってコントローラにアクセスしたときに、想定される結果を得られるかというテストです。モデルのデータ・ページの要素・リダイレクト先といったことをテストします。

ユニットテストライブラリ Test::Unit

Rails でのテストについて見ていく前に、Ruby のユニットテストライブラリである Test::Unit の使い方を確認しておきましょう。 Rails でのテストも Test::Unit を使います。

Test::Unit は、その名の通り、ユニットテストを行うためのライブラリです。

簡単に使い方を説明します。 まず、Test::Unit::TestCase を継承したクラスを用意し、そこに名前が test_ で始まるメソッドを定義します。 それから、アサート (assert) というテスト用のメソッドを使ってテストを書きます。

require 'test/unit'
class FooTest < Test::Unit::TestCase
  def test_foo
    assert 1 == 2, '1 は 2 と等しいはず'
  end
end

これを t3.rb という名前で保存して実行すると

$ ruby t3.rb
Loaded suite t3
Started
F
Finished in 0.016 seconds.

  1) Failure:
test_foo(FooTest) [t3.rb:4]:
1 は 2 と等しいはず.
<false> is not true.

1 tests, 1 assertions, 1 failures, 0 errors

こうなります。 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 のコマンドラインオプション

Test::Unit で作ったテストスクリプトに -h を付ければヘルプが出力され、コマンドラインオプションを確認できます。

 $ ruby t3.rb -h
 Test::Unit automatic runner.
 Usage: t3.rb [options] [-- untouched arguments]
 ...

いくつかあるオプションのうち、実行するテストを絞り込むのに使えるのが -n と -t です。

     -n, --name=NAME                  Runs tests matching NAME.
                                      (patterns may be used).
     -t, --testcase=TESTCASE          Runs tests in TestCases matching TESTCASE.
                                      (patterns may be used).
 $ ruby t3.rb -n /foo/

このように // で囲むと正規表現で指定できます。 テストを書きながら開発するときには、これらをうまく使って、すばやくテストを行うようにしましょう。

Rails でのテスト

Rails でのテストでも test/unit を使いますが、Rails アプリケーションをテストしやすいように拡張されています。

Rails でのテストについての詳細なドキュメントが次のところにあります (英語)。

フィクスチャ

フィクスチャとは、一般的には、テストに使う初期データのことをいいます。 assert で使う前に setup やテストメソッドで生成するオブジェクトがフィクスチャです。 ただ、Rails では、Active Record でテストデータベースを設定する機能を特に Fixture と呼んでいることから、 データベースまわりの少し狭い意味でフィクスチャと言っていることが多いようです。

フィクスチャを使う際に、特に注意しなければならないのは、association です。 テスト対象のモデルと association でつながっているモデルのフィクスチャを読み込むように指定しておかないと、予期しない現象に見舞われることがあります。

また、これは特に MySQL で MyISAM エンジンを使っている場合に当てはまりますが、トランザクションに対応していないデータベースを用いるときには、

# test/test_helper.rb:
self.use_transactional_fixtures = false

この指定を忘れないようにする必要があります。そうしないとテストの teardown でデータが元に戻らず、予期しない現象に見舞われることになります。

rake - テストをまとめて実行

Rails のテストをまとめて実行するときには、rake タスクを実行します。

プラグインのテストはアプリケーションのテストとは別に実行するようになっています。

テスト設計

ソフトウェアテストの基本的な考え方を紹介します。

テストを設計する際に決め手となるのは、入力と結果の組み合わせです。 この組み合わせをきちんと定義できれば、テストに書くことはおのずと定まります。

よく使われるブラックボックステストの手法をまとめておきます。

同値クラステスト

ある入力において、結果が同じ、あるいは処理が同じになる値は、テストの上では同値とみなせます。 入力条件ごとに有効同値 (条件を満たす値) と無効同値 (条件を満たさない値) を決めることができれば、その値の組み合わせだけテストすればよいことになります。

とはいうものの、‘はじめて学ぶソフトウェアのテスト技法’によれば、これは誰でも無意識のうちに使っている手法なのだそうです。

境界値テスト

同値クラステストから導かれる手法として、境界値テストがあります。

バグは境界に現れます。要件があいまいになるのはたいてい境界条件ですし、実装を誤るのは分岐条件や範囲の端っこでのことが多いからです。 入力条件の境界値と隣りの値を調べればバグを見つけやすくなります。

ドメインテスト

関連した 2 変数以上の境界値テストはドメインテスト (domain testing) として理論化されています。 境界値テストより少ない組み合わせで効率的にテストを実施できます。

ユニットテストの書き方

Rails のテストでは入力と結果をどのようにテストに落とし込んでいけばいいのでしょうか。

まず、モデルに対するユニットテストについては、Rails 固有の HowTo といったものは特になく、一般的な xUnit2 の知識がそのまま使えます。

しいていえば、 Active Record の仕組みを知っておくと、テストを書くとき役立ちます。 例えば、 Active Record はフェッチした値をキャッシュするので、reload (あるいは refresh) しないと、思い通りの結果を得られないことがあります。

初期データの選択

効率良くテストを行うにはどのような方針で初期データ (フィクスチャ) を選択すればよいのでしょうか。

  • 同値クラスの組み合わせを元に選択する

入力データとしてさまざまな組み合わせを試さないといけませんが、闇雲に値を選択しても効果が上がりません。 効率的にバグをあぶり出すためには、同値クラス・境界値・ドメインといった、ブラックボックステストの手法を元にするとよいでしょう。

  • 現実的な値

現実に即した値は代表値として使えます。現実的なデータを元にフィクスチャを用意するとよいでしょう。 このフィクスチャを元に、項目をひとつづつ無効値にずらしてやれば、同値クラステストを効率良く進めることができます。

  • 境界値

現実的な値とは別に、境界値をカバーする「極端な」フィクスチャを用意しておくと、境界値テストを効率良く進めることができます。

機能テストの書き方

ユニットテストを行って、モデルの動作を確認できたら、コントローラとビューに対する機能テストに進むことができます。

ほかの開発環境では、開発のはじめの段階では、機能テストまではなかなか手が回らないと思いますが、 Rails では開発者が自らの手で機能テストを気楽に書くことができます。

Rails の機能テストを書く際に知っておく必要があるのは、 入力であるブラウザからの操作をどのように記述すればいいのか、 また、期待される結果であるレスポンスをどのように記述すればいいのか、ということです。

入力の記述方法

機能テストを書くときは、ブラウザからパラメータがどのような形で渡るのかを把握しておく必要があります。

把握できたら、get, post メソッドを使って、リクエストをシミュレートします。

 def test_edit
   get :edit, :id => 1
   ...
 end
 def test_update
   post :update, :id => 1, :customer => {:name => 'tom'}
   ...
 end

また、セッションについては、@request.session に直接指定します。

 def setup
   ...
   @request.session["key"] = "value"
 end

パラメータに渡される値の範囲

ブラウザから渡されるパラメータを調べる前に、Rails のパラメータ params が受け取りうる値について指摘しておきます。

Web アプリケーションへの接続は、そのアプリケーションが用意したページに限定されません。ブラウザに限ったことでもありません。 クライアントを自分で書けば、アプリケーションに渡すパラメータはサーバが許す限り何でもありです。 さらに、Rails アプリケーションは XML, YAML 形式のパラメータを受け取ることができ、これらは Ruby オブジェクトに変換されて評価されます。 つまり、Rails アプリケーションが解釈できるオブジェクトなら、何でもパラメータとして渡すことができるのです。 セキュリティを考える際には、このことを念頭に置く必要があります。

ブラウザから渡されるパラメータ

未入力フィールドが渡す値を調べておきましょう。 ブラウザからのアクセスを前提にした話です。

テキストフィールド

テキストフィールドに何も入力せずに submit するとパラメータの値は空文字列 “” になります。

チェックボックス

チェックボックスにチェックを付けずに submit すると、パラメータには何も渡りません。値だけでなくそもそもキーが存在しないことになります。

 <%= form_tag :action => 'debug' %>
   <%= check_box_tag 'cb0', '1' %>
   <%= check_box_tag 'cb1', '1', true %>
   <%= submit_tag %>
 <%= end_form_tag%>

そのまま submit すると

 --- !map:HashWithIndifferentAccess
 commit: Save changes
 action: debug
 controller: example
 cb1: "1"

チェックを付け替えてみると

 --- !map:HashWithIndifferentAccess
 commit: Save changes
 action: debug
 cb0: "1"
 controller: example

デフォルトのチェックの状態に関係なく、チェックを付けたキーだけが現れます。

ただし、ヘルパメソッド check_box を使うと、チェックボックスと共に隠しフィールドが用意されるので、チェックボックスの処理を特別扱いしなくてよくなります。

 <%= check_box 'post', 'validated' %>

これは次のように展開されます。

 <input id="post_validated" name="post[validated]" type="checkbox" value="1" />
 <input name="post[validated]" type="hidden" value="0" />

ただし上で見たように、タグヘルパメソッド check_box_tag を使うときは、隠しフィールドは付きません。

ラジオボタン

ラジオボタンもチェックボックスと同じです。

 <%= radio_button 'post', 'category', 'rails' %>

これは次のように展開されます。

 <input id="post_category_rails" name="post[category]" type="radio" value="rails" />

チェックを付けないと

 --- !map:HashWithIndifferentAccess
 commit: Save changes
 action: debug
 controller: example

チェックを付けると

 --- !map:HashWithIndifferentAccess
 commit: Save changes
 post: !map:HashWithIndifferentAccess
   category: rails
 action: debug
 controller: example

ラジオボタンをグループのうちでひとつも選択せずに submit すると、パラメータには何も渡りません。

セレクション

ブラウザからアクセスすると、select タグの選択肢は必ずどれかひとつ選択した状態になっています。 ヘルパメソッド select で :include_blank => true を指定したときは未選択だと “” が渡ります。

日付や時刻を扱うときは、date_select, time_select, datetime_select といったヘルパメソッドで select タグの組を生成します。

date_select であればデフォルト値は今日の日付になります。

 birth(1i): "2006"
 birth(2i): "2"
 birth(3i): "5"

include_blank => true を指定したときは “” が渡ります。

 birth(1i): ""
 birth(2i): ""
 birth(3i): ""

日付や時刻を扱う際にはこのように奇妙な名前を持つ複数のパラメータが渡ります。 テストを書くときも、基本的にこれに合わせてパラメータを書かないといけません。

 post :update, :id => 1, :pet => {'birth(1i)' => 2006, 'birth(2i)' => 2, 'birth(3i)' => 5}

ただし、場合によっては直接 Date, Time オブジェクトを渡してもかまいません。

 post :update, :id => 1, :pet => {:birth => Date.new(2006, 2, 5)}

これは、アクションでパラメータがどのように扱われるかによって変わってきます。 しかし、このような書き方はあまりお勧めできません。 このテストに合わせて params[:pet][:birth] を使って実装してしまうかもしれません。 そうしてテストに成功したとしても、実際にブラウザからアクセスしたときには、そのようなパラメータは現れませんから、きっとエラーになってしまうでしょう。 機能テストの書き方としては間違っていると思います。

ファイルアップロード

Rails でファイルをアップロードするときのビューは、次のようになります。

 <%= form_tag({:action => 'upload_file'}, {:multipart => true}) %>
   <%= file_field_tag 'file' %>
   <%= submit_tag %>
 <%= end_form_tag %>

ここで問題になるのは、ファイルを参照せずに submit したとき、file に何が渡るかということです。 アップロード先のビューに次のデバッグコードを仕掛けてみると、

 <%= debug(params) %>
 {"commit"=>"Save changes", "action"=>"upload_file", "controller"=>"admin", "file"=>#<StringIO:0x3c96dd8>}

このようになり、ファイルを参照しないときは params[:file] に StringIO オブジェクトが設定されることが分かります。 この StringIO オブジェクトには特異メソッド original_filename, content_type が定義されており、それぞれ値は “” になっています。これをチェックに用いれば、アップロードされていない場合に対応できます。

さて、これを機能テストとして書くにはどうすればいいでしょう。 StringIO オブジェクトに特異メソッド定義をすれば Mock が作成できます。3

 file = StringIO.new('')
 (class << file; self; end).class_eval do
   define_method(:original_filename) { '' }
   define_method(:content_type) { 'application/octet-stream' }
 end
 post :upload_file, :file => file

結果の記述方法

結果を確認するために、専用のアサートメソッドを使います。

assert_response を使ってステータスコードを確認できます。

 assert_response :success
 assert_response :redirect
 assert_response(404)

success のときはレンダリングしたテンプレート名を assert_template で確認できます。

 assert_template 'edit'

また、テンプレートに渡されたインスタンス変数の値を assigns で確認できます。

 assert_equal customers(:tom), assigns(:customer)

session, flash の値はそれぞれ session, flash で確認できます。

また、テンプレートを元に出力された HTML の要素を assert_tag で確認できます。

 assert_tag :tag => 'input', :attributes => {:type => 'text', :name => 'customer[name]', :value => 'tom'}

redirect のときはリダイレクト先を assert_redirected_to で確認できます。

 assert_redirected_to :action => 'show', :id => 1

リダイレクト先まで確認したいときは follow_redirect を使ってリダイレクトをシミュレートできます。

パラメータの型

アサートを書くとき、パラメータの型が問題になることがあります。

具体的な例として、params[:id] の値を redirect_to の :id パラメータに指定することを考えてみましょう。 post に渡したパラメータを redirect のパラメータとしてそのまま渡してみます。

 test:
   post :update, :id => 1
   assert_redirected_to :action => 'show', :id => 1
 controller:
     redirect_to :action => 'show', :id => params[:id]

何の問題もないように見えますが、このテストは失敗します。 params[:id] は文字列 “1” になるからです。 よって、パラメータを整数に変換する必要があります。

     redirect_to :action => 'show', :id => params[:id].to_i

あるいはテスト側 assert_redirected_to のパラメータを文字列に変更します。 この場合はpostに渡すパラメータも文字列にそろえたほうが分かりやすいです。

   post :update, :id => '1'
   assert_redirected_to :action => 'show', :id => '1'

結局、整数・文字列どちらがいいのでしょうか。

ブラウザから送られるクエリは、Railsによって文字列要素を持つハッシュに変換されます (正確に言うと、このハッシュは入れ子になることがありますし、要素が配列になることもあります。どちらにしても末端の要素は文字列です) 。実際の動作に合わせる意味では、テストでも文字列のハッシュを使うほうが良いと言えます。

しかし実際には、数値にしたほうが良いことも多いでしょう。例えば、次の実装を見てみましょう。

     # redirect_toの:idに整数でないオブジェクトを渡すと .id の値が使われる
     redirect_to :action => 'show', :id => @customer

ここでいちいち .id を付けてから .to_s したり “#{}” で囲ったりするのはださいですよね。 これに対応するテストをパスさせるには、パラメータを文字列ではなく数値にする必要があります。

また、パラメータをそのまま評価すると何かと危険です。整数であるべきパラメータなら整数に変換してから使う、こうすれば、パラメータにおかしな値を渡す攻撃からシステムを守ることができます。

時刻に対するテスト

外部の環境に依存するテストは書きにくいものです。 典型的な問題として、時刻を扱うテストがあります。 時刻に対するテストを行うためには、どのように設計すればよいのか考えてみます。

動的なフィクスチャ

ひとつの解法としては、フィクスチャのほうを動的に設定することが考えられます。 AWDwR にもこの解法の例が載っています。

ここでは生年月日と年齢の関係を例に採ります。 今日が20歳の誕生日である顧客を用意する必要があれば、次のように書けます。

 tom:
   birth: <%= Date.today.years_ago(20) %>

タイムライン

今日の日付によって処理が変わることがあります。 特に月 (month) や日 (day) の値が重要な役割を担うときは、先に紹介した動的なフィクスチャではうまくいきません。 例えば、25日締めで年齢計算は翌月1日時点で行うことを考えてみてください。 基点となる日付の計算が必要になります。 そして計算式 (ロジック) がテスト側にあるのは本末転倒ですよね。

フィクスチャの誕生日を固定して、計算して得られる年齢をテストすることを考えてみましょう。

 tom:
   birth: 1986-03-01

計算を行うメソッドに、基点となる時刻を与えられるようにすれば、任意の基点についてテストできるようになります。

 class Customer
   def age(basetime)
   end
 end

また、Ruby の動的な力を利用して、テストのときだけ Time.now を再定義する方法もあります。

 class Time
   def self.now
     self.local(2006,2,25,0,0,0)
   end
 end

こうすれば、今日の日付を 2006-02-25 とみなしてテストを考えればよいことになります。

モデルの配列に対するテスト

コントローラで設定したモデルの配列 @pets が空であるかを調べるときは、先に紹介したように、 テンプレートに渡されたインスタンス変数の値を確認できる assigns を使って、

assert assigns(:pets).empty?

このように書けます。

あるいは配列の要素が 1 つのとき

assert_equal pets(:fluffy), assigns(:pets).first

これは問題ありませんね。

配列が要素を複数持つときには、いくつか問題があります。 ひとつめは、アサートの結果が分かりづらくなることです。

モデル (の配列) を assert の期待値にすると、inspect (pp) の出力にオブジェクトの持つ属性値の全てが含まれてしまいます。

assert_equal %w[fluffy claws].map {|fixture_name| pets(fixture_name)}, assigns(:pets)

わかりやすい属性値、例えば name に map しておくと、テストに失敗したとき見やすくなります。

assert_equal %w[Fluffy Claws], assigns(:pets).map {|pet| pet.name}

ただし、フィクスチャの name の値をそのまま期待値に書くと DRY に反するので (後述)、フィクスチャ名で指定すべきです。

assert_equal %w[fluffy claws].map {|fixture_name| pets(fixture_name).name}, assigns(:pets).map {|pet| pet.name}

問題点のふたつめは、配列要素の並び順です。

配列の大きさが 1 より大きいときに配列要素をチェックするには、 要素が並び替えられていることを実装の責任とする (つまり仕様に盛り込む) か、 順序は自由であるとし、テスト側で並び替えてからチェックする必要があります。

list アクションや検索結果を出力するアクションでは、得られた配列要素の順序が偶然のものなのか、それとも並び替えによる必然のものなのか、注意する必要があります。 データベースの仕様によっては、order by 句のない select の結果は primary key である id の順序で並んでいることを期待できますが、この振る舞いに依存しないほうがよいでしょう。 運用し始めは問題なかったのに、途中の行が削除されて間に挿入されたために、並び順に問題が生じたことがありました。 並び替えのし忘れは、結構やってしまいがちだと思うのですが、どうでしょう。

この暗黙の順序に依存しないように、フィクスチャに設定する id の順序を、あえて期待される順序とは異なるように入れ替えておくのも、良い対策だと思います。

テストと DRY 原則

テストを書くときでも DRY 原則は有効です。同じことは書かない。この約束を守るなら、テストはどう書くことになるでしょうか。

フィクスチャと DRY 原則

まず、前提条件としてのフィクスチャに注目しましょう。 フィクスチャは入力データであり、テストを書くときの基礎になります。 DRY 原則を守るなら、フィクスチャに書いたことは、ほかのところで書いてはいけない、といえます。

具体的には、例えば Customer モデル c の c.name をチェックするとします。 フィクスチャに

customers.yml:
tom:
  id: 1
  name: tom

とあり、

assert_equal 'tom', c.name

このように期待値として文字列 ‘tom’ を書くと、後でフィクスチャ上の値を変更したときにこちらも変更しなくてはいけません。

assert_equal customers(:tom).name, c.name

このようにフィクスチャの値を参照するように書くほうがよいことになります。

id についても同じことがいえます。 以前「パラメータの型」で次のような例を示しました。

post :update, :id => 1
assert_redirected_to :action => 'show', :id => 1

これは scaffold のテストでも見られる典型的なテストで、:id には数値を指定します。しかし、これは次のように書いたほうがよりよいように思います。

post :update, :id => customers(:tom).id
assert_redirected_to :action => 'show', :id => customers(:tom).id

フィクスチャにおける DRY 原則のメリット:

  • フィクスチャを修正したときの影響が小さくなる
  • 何をテストしているのかが明確になる

アサートをまとめる

DRY 原則を守るため、あるいは見通しをよくするため、いくつかのアサートをひとつのメソッドに括り出すことができます。

以前「モデルの配列に対するテスト」で、ペットの配列 @pets に対するアサートを取り上げました。これを何度も使うことになるなら、次のように括り出せます。

ちなみにこの pet 達は MySQL リファレンスマニュアルの pet データ を元にしています。

 def assert_pets(*fixture_names)
   assert_equal fixture_names.map {|fixture_name| pets(fixture_name).name}, assigns(:pets).map {|pet| pet.name}
 end
 def test_search_pet_with_blank_params
   post :search_pet, :name => '', :month => ' '
   assert_pets :fluffy, :claws
 end
 def test_search_pet_with_month
   post :search_pet, :name => '', :month => '2'
   assert_pets :fluffy
 end

括り出したメソッドの名前は test_ で始まらないようにします。そうしないと、テストメソッドとして呼ばれてしまいます。

アサートをまとめるときに注意したいのは、結果が同じ、あるいはテスト対象での処理が同じになるアサートを何度も繰り返す必要はないということです。あるテストメソッドを元に、条件となるパラメータを少し変えて別のテストメソッドを用意したとします。その条件の変更に影響しないアサートが含まれていれば、それは削除すればよく、括り出す必要はありません。

テストの重複を取り除く

テスト A が成功するならテスト B は必ず成功する。このとき、テスト B は削除してかまわない、といえます。

例として、scaffold で生成される次のテストを評価してみましょう。

assert_response :redirect  # (B)
assert_redirected_to ...   # (A)

assert_redirected_to はリダイレクトのパラメータが期待するものと一致するかをチェックします。リダイレクトすることが前提になりますから、これが成功するなら assert_response :redirect は必ず成功します。つまり、テスト A はテスト B を含むといえますから、テスト B は削除してもテストの強度は変わりません。

重複を取り除くメリット:

  • テストにかかる時間が短くなる
  • DRY 原則に沿う
  • 何をテストしているのかが分かりやすくなる

デメリット:

  • 段階的なテストではなく、一足飛びのテストになるので、バグの原因が分かりづらくなる
  • 失敗したときのメッセージが分かりづらくなる (「これこれのパラメータを伴ってリダイレクトしていません」よりも、単に「リダイレクトしていません」のほうが、失敗の理由がわかりやすい)

同値クラスの考え方を取り入れる

これはつまり、同じことを何度もテストしないということです。 テストは網羅できませんが、効率のよいテストを考えることはできます。 良いテストとは、バグを見つけるテストです。 バグを見つける効率を高めるために、異なるバグを見つけられるテストの組を設計すべきです。

先の 2 月生まれのペットの検索に加えて、8 月生まれのペットの検索を書いたとします。

 def test_search_pet_with_month_8
   post :search_pet, :name => '', :month => '8'
   assert_pets :fang, :bowser
 end

これらは確かに結果が異なっていますが、処理の面では同等です。 2 月で成功するのに 8 月で失敗する理由はなさそうです。 month=1..12 の範囲は、テストを行う上では同値であるとして差し支えありません。 ですから、あえて 8 月の検索を書く必要はないのです。

これに対して、month=’ ‘ のときは処理が異なるので (書き忘れていましたが、空白ならその項目を検索条件に使わないという要件になっています) テストを書く必要があります。

テストのリファクタリング

リファクタリングの方法は普通のコードと同じです (たぶん)。 ただ、テストのテストは普通書きませんから、テストのコードは見通しがよくないといけません。 こてこてに構造化して、何をアサートしているのか一目で分からなくなるのはよくないです。

テストの粒度

テストメソッドの粒度、テストクラスの粒度はどのように考えればよいのでしょうか。

用意されるテストの雛形に引きずられてしまいがちですが、 ひとつのクラスに対するテストクラスはひとつであると決まっているわけではありません。 むしろ、テストすることひとつに対してテストクラスを作ると、テストすることが明確になり、テストメソッド名が短くてすみます。 テストメソッド名に同じプレフィックスを持つものがたくさんできてきたら、クラスの分割を検討すべきです。

カバレッジツール

ここまでブラックボックステストの考え方に倣って、効率の良いテストの書き方を考えてきましたが、 ソフトウェアテストにはもうひとつ、ホワイトボックステストという考え方があります。 こちらはコードを解析して経路を網羅しようという、パス網羅という考え方が基本にあります。

網羅のことをカバレッジ (coverage) というのですが、書いたテストがどの程度コードを網羅しているのか調べるためのツールが作られています。それがカバレッジツールです。

ここではいくつかある Ruby/Rails 用のカバレッジツールを紹介します。 いずれも行指向のカバレッジで、条件網羅の面から見ると弱いのですが、テストを書くときの目安として役立ちます。

rcov

rcov は Mauricio Fernandez さん作の Ruby 用のカバレッジツールです。

インストール

rcov は RPA (Ruby Production Archive) のパッケージとして配布されていますので、先に rpa-base をインストールする必要があります。rpa-base をインストールすると rpa コマンドが使えるようになります。 rcov のインストールは

 $ rpa install rcov

で行えます。 なお、最新版が eigenclass - Better code coverage for Ruby: rcov 0.1.0 prerelease で配布されているようです。こちらは rpa-base をインストールしなくてもダウンロードできます。

($ su)
 # ruby setup.rb

rcov コマンドがインストールされます。 拡張ライブラリを make できる環境では rcovrt.so もインストールされます。 set_trace_func の替わりに Cレベルで rb_add_event_hook を行い、高速に動作するようです。

使い方

 $ rcov test.rb

Test::Unit で記述した任意のテストスクリプトを引数にして rcov コマンドを実行します。 すると coverage/ ディレクトリに結果が出力されます。

insurance

insuranceLunchbox Software さん作の Ruby/Rails 用のカバレッジツールです。

インストール

 $ gem install insurance

railscov コマンドと拡張ライブラリ insurance_tracer.so がインストールされます。

使い方

 $ railscov -A MyRailsApp

Rails アプリケーションのディレクトリに rake タスクをインストールします。

 $ rake insurance

rake タスクを実行します。 すると insurance/ ディレクトリに結果が出力されます。

著者について

もりきゅうは ミッタシステム のプログラマです。

著者の連絡先は moriq@moriq.com です。

RubyOnRails を使ってみる 連載一覧


  1. ドライバは上位モジュールの代替プログラム、スタブは下位モジュールの代替プログラムです。Rails のテストでは、テストの実行環境を用意する test_help.rb がドライバ、ActiveRecordの test 環境や TestRequest, TestResponse がスタブといえそうです。 

  2. テスティングフレームワークの総称。各言語用に開発されています。 

  3. A Guide to Testing the Rails: 9. Testing Your Controllers の “9.7 Testing File Uploads” より