RailsのテストフレームワークRSpecの基礎知識
実践Ruby on Rails 4 現場のプロから学ぶ本格Webプログラミング
顧客管理システムの構築を体験しながら、Railsアプリケーション開発のノウハウを習得! この記事は、書籍『実践Ruby on Rails 4 現場のプロから学ぶ本格Webプログラミング』の内容を、Think IT向けに特別にオンラインで公開しているものです。詳しくは記事末尾の書籍紹介欄をご覧ください。エグザンプル
RSpecは「ビヘイビア駆動開発(Behavior Driven Development:BDD)」というプログラム開発手法をRubyで実践するために作られたテストフレームワークです。従来の開発手法との違いを強調するためか、RSpecは独特の用語を採用しています。その1つがエグザンプル(example)です。
さきほど作成したstring_spec.rbでexampleとendで囲まれた部分がありました。
example '文字の追加' do s = "ABC" s << "D" expect(s.size).to eq(4) end
これがエグザンプルです。exampleメソッドの引数には、このエグザンプルを説明する短い文を指定します。
exampleメソッドには、itという別名があります。これを利用すると、先ほどの例は次のように書けます。
it 'appends a character' do s = "ABC" s << "D" expect(s.size).to eq(4) end
エグザンプルの説明を英語で記述する場合、itと引数で1つの文のように見えるため、この別名がしばしば使われます。exampleメソッドには、さらにspecifyという別名もあります。
もちろん日本語で説明を記述する場合でもitを使用して構いませんが、私は違和感があるため使いません。
さて、エグザンプル(example)を日本語に直訳すれば「例」あるいは「用例」です。この用語は、伝統的な用語で言えばテストケース(test case)に相当します。すなわち、ソフトウェアのある機能の前提条件と期待される結果をコードで表現したものです。
なぜRSpecはそれをエグザンプルと呼ぶのでしょうか。それは、ビヘイビア駆動開発では、テストコードによってソフトウェアの仕様(specification)が定義される、と考えるからです。RSpecという名前もそこに由来しています。RSpecは単にソフトウェアのテストをするだけでなく、ソフトウェアの仕様を記述することに重点を置いています。ある意味では、テストコードが仕様書の代わりになるのです。仕様は言葉で表現するよりも、実際の用例を列挙した方がわかりやすいものです。
エグザンプルグループ
RSpecではいくつかの関連するエグザンプルをエグザンプルグループ(example group)としてまとめることができます。先ほどのstring_spec.rbをご覧ください。
require 'spec_helper' describe String do describe '#<<' do example '文字の追加' do (省略) end end end
describeとendで囲まれた部分がエグザンプルグループです。describeメソッドの引数には、クラスまたは文字列を指定します。エグザンプルグループは入れ子構造にすることが可能です。ここでは、全体としてStringクラスに関する仕様をまとめたエグザンプルグループを作った後、その内側に
テスト結果の読み方
当然のことながら、テストは失敗することもあります。テストが失敗するとどんな結果になるのか見るため、string_spec.rbに意図的に失敗するエグザンプルを追加してみましょう。
spec/experiments/string_spec.rb
require 'spec_helper' describe String do describe '#<<' do example '文字の追加' do s = "ABC" s << "D" expect(s.size).to eq(4) end example 'nilの追加' do s = "ABC" s << nil expect(s.size).to eq(4) end end end
実行してみましょう。
$ bin/rspec spec/experiments/string_spec.rb .F Failures: 1) String#<< nilの追加 Failure/Error: s << nil TypeError: no implicit conversion of nil into String # ./spec/experiments/string_spec.rb:13:in `block (3 levels) in <top>' # -e:1:in `<main>' Finished in 0.09223 seconds 2 examples, 1 failure Failed examples: rspec ./spec/experiments/string_spec.rb:11 # String#<< nilの追加 </main></top>
結果の1行目にはドット(.)とFという文字が出力されています。複数のエグザンプルを実行する場合、その実行順はランダムなので、もしかするとFが先に表示されているかもしれません。これらの文字の数はエグザンプルの数と同じです。ドットは成功、Fは失敗を意味します。
Failures:以下には、失敗したエグザンプルについて説明が表示されています。「String#
TypeError: no implicit conversion of nil into String
ここから例外TypeErrorが発生したことがわかります。「nilは暗黙裏にStringに変換されない」と説明が書かれています。その下のシャープ記号(#)で始まる3行は、エラーのバックトレース(呼び出し履歴)です。
pending
テストが失敗したら、通常はテスト対象のクラスかspecファイルのいずれかに誤りがあるということですので、すべてのテストが成功するまでソースコードの修正とbin/rspecコマンドの実行を繰り返すことになります。しかし、原因がわからないとか時間が足りないといった理由ですぐには直せないこともあります。その場合は、次のようにpendingメソッドを使ってエグザンプルに「未解決(pending)」の印を付けます。
spec/experiments/string_spec.rb
(省略) example 'nilの追加' do pending('調査中') s = "ABC" s << nil expect(s.size).to eq(4) end end end
pendingメソッドの引数には、未解決である理由などを表す文字列を指定してください。この状態でテストを実行すると結果は次のようになります。
*. Pending: String#<< nilの追加 # 調査中 # ./spec/experiments/string_spec.rb:12 Finished in 0.12296 seconds 2 examples, 0 failures, 1 pending
1行目のアスタリスク(*)は未解決の状態にあるエグザンプルを表します。5行目にpendingメソッドに与えた文字列が表示されています。
pendingメソッドを書くのが面倒な場合には、exampleをxexampleに書き換えるという方法もあります。
spec/experiments/string_spec.rb
require 'spec_helper' describe String do describe '#<<' do (省略) xexample 'nilの追加' do s = "ABC" s << nil expect(s.size).to eq(4) end end end
xexample 'nilの追加' do s = "ABC" s << nil expect(s.size).to eq(4) end end end
この場合は、テストの実行結果は次のようになります。
*. Pending: String#<< nilの追加 # Temporarily disabled with xexample # ./spec/experiments/string_spec.rb:12 Finished in 0.12296 seconds 2 examples, 0 failures, 1 pending
4行目に「Temporarily disabled with xexample」と出力されています。「xexampleにより一時的に無効化されている」という意味です。
expectメソッドとマッチャー
●オブジェクトを対象にする場合
さて、ここまで後回しにしてきたexpectメソッドについて説明しましょう。expectメソッドの一般的な使用法は、以下のとおりです。
expect(T).to M
Tをターゲット、Mをマッチャーと呼びます。string_spec.rbでは次のように書かれていましたね。
expect(s.size).to eq(4)
ターゲットはs.sizeメソッドが返すオブジェクト、マッチャーはeq(4)が返すオブジェクトです。
マッチャーは、ターゲットに指定されたオブジェクトがある条件を満たすかどうか(マッチするかどうか)を調べるオブジェクトです。ターゲットがその条件を満たさなければ、そのエグザンプルは失敗したと判定されます。
eqメソッドは引数に指定したオブジェクトとターゲットが等しいかどうかを調べるマッチャーを返します。string_spec.rbの場合は、s.size == 4であれば成功、そうでなければ失敗です。
expectメソッドの後ろに付いているtoをnot_toに変えると、全体の意味が反転します。
expect(s.size).not_to eq(4)
s.sizeが4に等しくなければ成功で、等しければ失敗になります。
RSpecには数多くのマッチャーが用意されています。本書を通じて少しずつ紹介していきます。
●ブロックを対象にする場合
expectメソッドにはexpect(T).to Mの他にもう1つの使用法があります。
expect { ... }.to M
{ ... }はブロックです。実例をお見せしましょう。string_spec.rbを次のように修正してください。
spec/experiments/string_spec.rb
(省略) example 'nilは追加できない' do s = "ABC" expect { s << nil }.to raise_error(TypeError) end end end
raise_errorマッチャーは、expectメソッドに指定されたブロックが特定の例外を発生させることを確かめます。ここではs
エグザンプルの絞り込み
●行番号による絞り込み
すでに見たようにbin/rspecコマンドにspecファイルのパスを指定すれば、そのspecファイルに記述されたエグザンプルだけを実行できます。しかし、特定のspecファイルの中の特定のエグザンプルだけを実行したい場合もあります。そのときは、次のように--line-numberオプション(または-lオプション)で行番号を指定してください。
$ bin/rspec spec/experiments/string_spec.rb --line-number 11 $ bin/rspec spec/experiments/string_spec.rb -l 11
この場合は、11行目で定義されているエグザンプルだけが実行されます。
あるいは、次のようにパスの後ろにコロン(:)と行番号を指定しても同じ結果となります。
$ bin/rspec spec/experiments/string_spec.rb:11
●タグによる絞り込み
次にタグを使ってエグザンプルを絞り込む方法を紹介します。string_spec.rbを次のように修正してください。
spec/experiments/string_spec.rb
(省略) example 'nilは追加できない', :exception do s = 'ABC' expect { s << nil }.to raise_error(TypeError) end end end
2つ目のexampleメソッドの第2引数に加えた:exceptionというシンボルがタグです。こうしておくと、次のコマンドで:exceptionタグの付いたエグザンプルだけをまとめて実行できます。
$ bin/rspec spec --tag=exception