RailsのテストフレームワークRSpecの基礎知識
実践Ruby on Rails 4 現場のプロから学ぶ本格Webプログラミング
顧客管理システムの構築を体験しながら、Railsアプリケーション開発のノウハウを習得! この記事は、書籍『実践Ruby on Rails 4 現場のプロから学ぶ本格Webプログラミング』の内容を、Think IT向けに特別にオンラインで公開しているものです。詳しくは記事末尾の書籍紹介欄をご覧ください。RSpecの基礎知識
本記事では、テストフレームワークとしてRSpecを採用します。RSpecをうまく活用すると、簡潔で読みやすいテストコードを書くことができ、Railsアプリケーションの保守性を高めることができます。
しかし、RSpecの用語法や表記法はやや独特で、慣れるまでには時間がかかります。読者の中にはとまどいを覚える方がいらっしゃるかもしれませんが、次章以降を読み進めるうえでの鍵となりますので、是非じっくりと読んで理解してください。
テストとは
Webアプリケーション開発の文脈において、テストという言葉はさまざまな意味で用いられます。日常的な場面では、人がWebアプリケーションの動きを目視でチェックする作業を意味します。たとえば、Webアプリケーションをサーバーコンピュータにインストールし、人が実際にWebブラウザを操作して、仕様どおりの反応を返すかどうか確かめるといった作業です。
しかし、プログラマの間では「テスト」という言葉がしばしば異なる意味で用いられます。私たちは、専用のプログラムによってWebアプリケーションの動作を確認することを「テスト」と呼びます。この意味でのテストは自動で行われます。人間がディスプレイを見守っていなくてもテストが進行し、テストが終わると成功あるいは失敗という結果がディスプレイに表示されます。
本書では「テスト」という言葉を、ソフトウェアによって自動で実施されるテストという意味で用います。
テストを実行するための専用のプログラムを「テスト」と呼ぶ用法もあります。たとえば、私たちはしばしば「テストを書く」という言い方をします。これは、テストを実行するためのプログラムを作ることを意味します。
RSpecとは
前著『改訂新版 基礎Ruby on Rails』では、Railsに標準で組み込まれているテストフレームワークTest::Unitを用いたテストについて解説しましたが、本書ではRSpec(アールスペック)という別のテストフレームワークを採用することにします。
Rails 4では標準のテストフレームワークがTest::UnitからMiniTestに変更されました。MiniTestはTest::Unitの機能強化版で、Test::Unitとの互換性をほぼ保っています。
RSpecを採用した理由は、正直に言えば「私が普段使っているから」ということになります。RSpecはRailsプログラマの間で高い人気を獲得していますが、その独特の用語法や書き方になじめない方も多いようです。私はRSpecにはTest::Unitやその後継のMiniTestにはない優れた特徴があると考えています。
しかし、RSpecがRailsに組み込まれていないことからもわかるように、けっして「事実上の標準(de facto standard)」の地位をRails業界において確立しているわけではありません。
RSpecの初期設定
RailsでRSpecを利用する場合、一度だけ次のコマンドを実行する必要があります。
$ bin/rails g rspec:install
ゲストOSで実行してください。すると、specディレクトリが作られて、その下にspec_helper.rbというファイルが生成されます。初期状態での内容は次のとおりです(コメント行を省略しています)。
spec/spec_helper.rb
ENV["RAILS_ENV"] ||= 'test' require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } ActiveRecord::Migration.check_pending! if defined?(ActiveRecord::Migration) RSpec.configure do |config| config.fixture_path = "#{::Rails.root}/spec/fixtures" config.use_transactional_fixtures = true config.order = "random" end
Rails 4.1に対応するため、このファイルの7行目を次のように書き換えてください。
ActiveRecord::Migration.maintain_test_schema!
RSpec ―― はじめの一歩
RSpecがどんなものであるかを感覚的に理解するため、実際にRSpecによる簡単なテストコードを書いて、実行してみることにしましょう。
Baukisのプロジェクトディレクトリ直下にspecディレクトリがあります。その下にexperimentsというサブディレクトリを作り、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 end end
RSpecのテストコードは、通常specディレクトリの下に置きます。ファイル名の末尾は_spec.rbで終わるようにしてください。これらのファイルをspecファイルと呼びます。
specファイルはspecディレクトリのサブディレクトリに適宜分類して配置します。どのサブディレクトリにどんなspecファイルを置くかは慣習的に決まっています。たとえば、モデルクラスに関するspecファイルはspec/modelsディレクトリに、APIに関するspecファイルはspec/requestsディレクトリに置くのが一般的です。しかし、独自のサブディレクトリを用意しても構いません。ここでは、RubyもしくはRailsの仕様に関する実験を行うspecファイルを置く場所としてexperimentsサブディレクトリを作成しました。これは本書独自のルールです。
テストコードの本体は次の3行です。
s = "ABC" s << "D" expect(s.size).to eq(4)
まず変数sに"ABC"という文字列をセットし、それに"D"という文字を追加しています。最後に、expectメソッドで変数sの状態を調べています。expectメソッドの使い方は後述しますが、ここではsの長さが4であるかどうかをチェックしています。
では、このspecファイルを実行してみましょう。ゲストOSのターミナルで、次のコマンドを実行してください。
$ bin/rspec spec/experiments/string_spec.rb
すると、次のような結果がターミナルに表示されます。
. Finished in 0.13482 seconds 1 example, 0 failures Randomized with seed 15657
ただし、3行目の「0.13482」および6行目の「15657」は、実行するたびに値が変化します。この結果の読み方については、後述します。
bin/rspecコマンドの結果の最終行に出力される「Randomized with seed ...」という行は、RSpecが内部的に利用している乱数のシード値(疑似乱数を計算するための係数)を表しています。RSpecは複数のエグザンプルをランダムな順番で実行するために乱数を利用しています。これ以降、テストの結果を掲載する際にはこの行を省略します。
このようにbin/rspecコマンドはspecファイルのパスを指定して個別に実行することも可能ですが、specファイルを含むディレクトリを指定して、そこに含まれるspecファイルを一括して実行することもできます。たとえば、次のコマンドはspecディレクトリ以下にあるすべてのspecファイルを実行します。
$ bin/rspec spec
実践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クラスに関する仕様をまとめたエグザンプルグループを作った後、その内側に<<メソッドに関する仕様をまとめるためのエグザンプルグループを作っています。<<メソッドの働きの一例が、「文字の追加」というエグザンプルとして表現されているのです。なお、4行目のメソッド名の頭にあるシャープ記号(#)は、インスタンスメソッドであることを示す慣用的な記号です。RSpecにとって何か特別な意味があるわけではありません。
テスト結果の読み方
当然のことながら、テストは失敗することもあります。テストが失敗するとどんな結果になるのか見るため、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#<< nilの追加」という文字列は、エグザンプルグループとエグザンプルの説明文を連結したものです。Failure/Error:の右側にあるs << nilは、失敗した箇所のコードです。その下にエラーメッセージが出力されています。
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 << nilという式を評価して、例外TypeErrorが発生すれば成功で、発生しなければ失敗となります。
エグザンプルの絞り込み
●行番号による絞り込み
すでに見たように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
テスト環境のセットアップ
RSpecによるテストを円滑に進めるため、spec/spec_helper.rbを変更してテスト環境を整えましょう。
テスト環境のためのGemパッケージ群
spec/spec_helper.rbの変更を始める前に、前章でGemfileに組み込んだテストに関連するGemパッケージの役割について簡単に紹介しておきます。
rspec-rails
RSpecをRuby on Railsに組み込むためのGemパッケージです。現在RSpecには1から3まで3種類のバージョンがあり、それぞれ仕様が大きく異なります。本書の記述はバージョン3に基づいています。
本書執筆時点におけるRSpecの最新版は2.14.2ですが、本書では2014年2月にリリースされたRSpec 3.0.0.beta2を採用します。
spring-commands-rspec
bin/rspecコマンドをSpringに対応させるためのGemパッケージです。
capybara
Capybara(カピバラ)のGemパッケージです。CapybaraはWebブラウザとRailsアプリケーションの間の通信をエミュレート(模倣)するためのライブラリです。たとえば、Webページ上の特定のボタンを探してクリックし、その結果のHTML文書を取得するといったことがCapybaraには可能です。MiniTestやRSpecと組み合わせて利用します。詳しくはChapter 19で説明します。
本書執筆時点におけるCapybaraの最新版は2.2.1です。
factory_girl
データベースにテストデータを投入するFactory GirlのGemパッケージツールです。このツールを使用するために、私たちはモデルごとにファクトリーを定義します。詳しくはChapter 9で説明します。
本書執筆時点におけるFactory Girlの最新版は4.4.0です。
Factory Girlの初期設定
RSpecでFactory Girlを利用する場合は、spec/spec_helper.rbを次のように書き換えてください。
spec/spec_helper.rb
(省略) RSpec.configure do |config| config.fixture_path = "#{::Rails.root}/spec/fixtures" config.use_transactional_fixtures = true config.order = "random" config.include FactoryGirl::Syntax::Methods config.before(:suite) do FactoryGirl.reload end end
まず14行目で、FactoryGirl::Syntax::Methodsモジュールで定義されているcreateやbuildなどのメソッドをテストの中で使用できるようにしています。
16~18行のconfig.before(:suite) do ... endの内側では、スイートの前に実行すべき処理を記述します。スイートとは、bin/rspecの実行対象となっているエグザンプルの集合を指す言葉です。つまり、bin/rspecコマンドが起動されてから最初のエグザンプルが実行されるまでの間に1回だけ実行すべき処理がここに記述されます。
ここではFactory Girlをリロードしています。この処理が必要となる原因はSpring(49ページ)にあります。SpringはRSpecの起動時間を短縮するため、RSpecの実行が終了した後もバックグランドでプロセスを起動したままにします。そのため、私たちがファクトリーの定義を変更しても、自動的にはRSpecによるテストに反映されないのです。
この記事のもとになった書籍 | |
---|---|
黒田 努 著 |
実践Ruby on Rails 4 現場のプロから学ぶ本格Webプログラミング本書は、Ruby on Railsの実践的な学習書です。最新のRuby2.0およびRuby on Rails4.1、RSpec3.0に対応しました。1つの企業向け顧客管理システムを作る中でRailsによるWebアプリケーション開発の基礎知識とさまざまなノウハウを習得していきます。各章末には演習問題が設けられているので、理解度を確かめながら確実に読み進められます。 |