RSpecとはBDDで使う素晴らしいツールです。(BDDとはbehavior-driven developmentの略で方向性を人の読める仕様に沿って開発を行う開発方法論です。)
ウェブで既に沢山の使い方や _何_ ができるかを説明したRSpecの書き込みを探せますが、RSpecで良いテストの作り方を説明した書き込みはなかなか探せません。
Better Specsは大抵のガイドラインの書いてない部分を集めようとしました。- これは開発者達の経験を通して学んだ方法です。
メソッドの説明をする
作成中のメソッドを明らかにしましょう。例えば、Ruby文書の規約ではクラスメソッドの名前には.
(もしくは::
)をインスタンスメソッドの名前には#
を使っています。
bad
describe 'the authenticate method for User' do
describe 'if the user is an admin' do
good
describe '.authenticate' do
describe '#admin?' do
Contextsを使う
Contextsはテストを明らかにし、まとめる素晴らしい方法です。 長い目で見ると、この方法はテストを読みやすくします。
bad
it 'has 200 status code if logged in' do
expect(response).to respond_with 200
end
it 'has 401 status code if not logged in' do
expect(response).to respond_with 401
end
good
context 'when logged in' do
it { is_expected.to respond_with 200 }
end
context 'when logged out' do
it { is_expected.to respond_with 401 }
end
Contextsでは"when"もしくは"with"で説明し始めましょう。
説明を短く
specの説明は40文字を超えないようにしましょう。超えた場合はcontextを分けてください。
bad
it 'has 422 status code if an unexpected params will be added' do
good
context 'when not valid' do
it { should respond_with 422 }
end
この例ではit { should respond_with 422 }
と書いてステータスの説明を削除しました。
rspec filename
でテストを実行すると相変わらず読める出力を取得できます。
Formatted Output
when not valid
it should respond with 422
単一条件テスト
'単一条件'は'各テストは一つだけ確認すべき'という表現でより広く知られています。 これはエラーを探しやすくし、失敗するテストをすぐ見つけるようにし、コードを読みやすくします。
独立したユニットでは、各例はただ一つの振る舞いだけテストするのが望ましいです。例の中で多数のテストがあるのは幾つかの振る舞いに分離する必要があることを示しています。
しかし、分離できないテストで(例えばDBや外部システムとの連動、前後があるテストの場合)分離するだけでは同じセットアップを何回も行い、テストが重くなる現象が現れます。こういった重いテストは分けなくても良いでしょう。
good (isolated)
it { should respond_with_content_type(:json) }
it { should assign_to(:resource) }
Good (not isolated)
it 'creates a resource' do
expect(response).to respond_with_content_type(:json)
expect(response).to assign_to(:resource)
end
可能な限り全部をテスト
テストはやった方がいいですが、全ケースをテストしないと、有用とはいえません。有効な場合と無効な場合を全部テストしましょう。例えばこんなアクションがあるとしましょう。
Destroy action
before_filter :find_owned_resources
before_filter :find_resource
def destroy
render 'show'
@consumption.destroy
end
普段よく見るエラーで、モデルがうまく削除できたかだけテストしています。 が、少なくとも二つのエッジケースが存在します。モデルを探せなかった時と権限がない時です。 すべての一般的に可能な入力を考えてテストしましょう。
bad
it 'shows the resource'
good
describe '#destroy' do
context 'when resource is found' do
it 'responds with 200'
it 'shows the resource'
end
context 'when resource is not found' do
it 'responds with 404'
end
context 'when resource is not owned' do
it 'responds with 404'
end
end
Expect対Should
新しいプロジェクトではexpect
だけ使いましょう。
bad
it 'creates a resource' do
response.should respond_with_content_type(:json)
end
good
it 'creates a resource' do
expect(response).to respond_with_content_type(:json)
end
全体で新しい文法だけ許容し、Rspec2の文法を使えなくなる様に設定できます。
good
# spec_helper.rb
RSpec.configure do |config|
# ...
config.expect_with :rspec do |c|
c.syntax = :expect
end
end
Subjectを使う
もしも、同じsubjectに対して複数のテストをしていたら、subject{}
を使ってDRYしましょう。
bad
it { expect(assigns('message')).to match /it was born in Belville/ }
good
subject { assigns('message') }
it { should match /it was born in Billville/ }
RSpecでは名前付きのsubjectも使えます。
Good
subject(:hero) { Hero.first }
it "carries a sword" do
expect(hero.equipment).to include "sword"
end
rspec subjectに関してもっと学ぶ。
letとlet!を使う
変数に値を入れる必要がある時はbefore
ブロックの代わりにlet
を使いましょう。 let
を使えば変数が初めて使用された時だけlazy loadしてspecテストが終わるまでキャッシュとして使えます。
このstackoverflowの答にlet
に関しての詳しい説明があります。
bad
describe '#type_id' do
before { @resource = FactoryGirl.create :device }
before { @type = Type.find @resource.type_id }
it 'sets the type_id field' do
expect(@resource.type_id).to equal(@type.id)
end
end
good
describe '#type_id' do
let(:resource) { FactoryGirl.create :device }
let(:type) { Type.find resource.type_id }
it 'sets the type_id field' do
expect(resource.type_id).to equal(type.id)
end
end
let
を初期化に使うとspecをテストする時にlazy loadされます。
good
context 'when updates a not existing property value' do
let(:properties) { { id: Settings.resource_id, value: 'on'} }
def update
resource.properties = properties
end
it 'raises a not found error' do
expect { update }.to raise_error Mongoid::Errors::DocumentNotFound
end
end
ブロックが定義した時に変数を定義したい時はlet!
を使いましょう。
これはDBのクエリーやスコープのテストで有用です。
これはletの実体の説明です。
good
# this:
let(:foo) { Foo.new }
# is very nearly equivalent to this:
def foo
@foo ||= Foo.new
end
rspec letに関してもっと学ぶ
Mockを使うか使わないか
まだ議論中のものです。出来るだけ(過度に)mockを使わずに実際の振る舞いをテストしましょう。 実際のケースをテストするのはロジックを変更する際に有用です。
good
# simulate a not found resource
context "when not found" do
before do
allow(Resource).to receive(:where).with(created_from: params[:id])
.and_return(false)
end
it { should respond_with 404 }
end
mockはspecを早くしますが正しく使うのが難しいです。mockをうまく使うにはもっとmockを理解する必要があります。この書き込みをご覧ください。
必要なデータだけ作る
中小規模のプロジェクトの経験しかないと、テストスイートは重くなりがちです。必要以上のデータをロードしないようにしましょう。多数のデータが必要だと考えるならば、何かが間違っている可能性があります。
good
describe "User"
describe ".top" do
before { FactoryGirl.create_list(:user, 3) }
it { expect(User.top(2)).to have(2).item }
end
end
fixtureの代わりにfactoryを使う
これは古い話ですが、話しておく価値があります。fixtureは使わないでください。なぜならfixtureは操作が難しいからです。代わりにfactoryを使いましょう。factoryを使うと新しいデータを生成する際のコードの量を減らせます。
bad
user = User.create(
name: 'Genoveffa',
surname: 'Piccolina',
city: 'Billyville',
birth: '17 Agoust 1982',
active: true
)
good
user = FactoryGirl.create :user
注意!この文書でのユニットテストはfixtureもfactoryも使っておりません。 複雑なfixtureやfactoryを作る時間を節約すると、ライブラリにロジックを追加する時間を稼げます。 この書き込みを読んでみましょう。
Factory Girlをもっと学ぶ。
読みやすいmatcherを使う
読みやすいmatcherを使いましょう。 rspec matcherを二回以上確認しましょう。
bad
lambda { model.save! }.to raise_error Mongoid::Errors::DocumentNotFound
good
expect { model.save! }.to raise_error Mongoid::Errors::DocumentNotFound
Shared Examples
テストを作るのは素晴らしいです。毎日少しづつ自信がつきます。が、結局色んな所にコードの重複が発生します。shared exampleを使ってテストをDRYしましょう。
bad
describe 'GET /devices' do
let!(:resource) { FactoryGirl.create :device, created_from: user.id }
let(:uri) { '/devices' }
context 'when shows all resources' do
let!(:not_owned) { FactoryGirl.create factory }
it 'shows all owned resources' do
page.driver.get uri
expect(page.status_code).to be(200)
contains_owned_resource resource
does_not_contain_resource not_owned
end
end
describe '?start=:uri' do
it 'shows the next page' do
page.driver.get uri, start: resource.uri
expect(page.status_code).to be(200)
contains_resource resources.first
expect(page).to_not have_content resource.id.to_s
end
end
end
good
describe 'GET /devices' do
let!(:resource) { FactoryGirl.create :device, created_from: user.id }
let(:uri) { '/devices' }
it_behaves_like 'a listable resource'
it_behaves_like 'a paginable resource'
it_behaves_like 'a searchable resource'
it_behaves_like 'a filterable list'
end
経験によると、shared exampleは対体コントローラで使われます。モデルはお互いかなり異なるため、(普通は)多くのロジックを共有しません。
rspec shared examplesに対してもっと学ぶ。
見えるものをテスト
モデルとアプリの振る舞いを深くテストしましょう(結合テスト)。 コントローラをテストする為にいらない複雑さを入れないようにしましょう。
私は自分のアプリケーションをテストする時、最初はコントローラをテストしていましたが、今は行いません。 今はRSpecとCapybaraの統合テストしか作りません。なぜかと言うとほんとに目に見えるものをテストするべきだと信じているし、コントローラをテストするのは不要な段階だと思っているからです。そのうちテスト達はモデルと統合テストにされ、shared examplesに纏まって、きれいで読みやすいテストを作るようになります。
これは未だ Ruby コミュニティーで議論中の話で、両方の側その考えを支える良い根拠があります。コントローラのテストを支持する側は統合テストは遅くてすべての機能をカバー出来ないと主張します。
両方とも違います。大体簡単にすべての機能をカバー出来るし(でしょ?)Guardみたいな自動化ツールを利用して一ファイルだけテストするのも可能です。こうすれば大きな流れを逆らわない範囲内で必要なspecだけテストできます。
shouldを使わない
テストの説明を書くときにshould使わないようにしましょう。現在時制に第三者観点から書きましょう。 新しいexpectationを使うともっと良いです。
bad
it 'should not change timings' do
consumption.occur_at.should == valid.occur_at
end
good
it 'does not change timings' do
expect(consumption.occur_at).to equal(valid.occur_at)
end
shouldを使わないように制限できるshould_notをご覧ください。 そして既に作成されたrspecから"should."で始まる例文を消してくれるthe should_cleanもご覧ください。
guardを使ったテスト自動化
変更がある度に全テストをまわすのは厄介です。それは時間もかかりますし、流れをわずわらせます。Guardを使うと更新された使用、モデル、コントローラ、ファイルだけに対してテストをまわせるように自動化できます。
good
bundle exec guard
good
guard 'rspec', cli: '--drb --format Fuubar --color', version: 2 do
# すべての更新されたspecを実行
watch(%r{^spec/.+_spec\.rb$})
# lib/フォルダーの中のファイルが変更された時libのspecを実行
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
# モデルが変更された時関連モデルのspecを実行
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
# ビューが変更された時関連ビューのspecを実行
watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
# コントローラが変更された時コントローラと関連ある統合テストのspecを実行
watch(%r{^app/controllers/(.+)\.rb}) { |m| "spec/requests/#{m[1]}_spec.rb" }
# application_controllerが変更された時すべての統合テストのspecを実行
watch('app/controllers/application_controller.rb') { "spec/requests" }
end
Guardは良いツールですが、求めるすべてを満足させません。環境によってはTDDで働きながらキー設定で自分が望むタイミングで実行するのが良い時もあります。そしてpushの前だけ全体テストをまわしましょう。ここに参考になるvimの設定があります。
sporkを使ったテスト高速化
Railsでテストを実行するにはRails全体のロードが必要です。これでは時間がかかり仕事の邪魔になります。 Zeus、 Spin、 Sporkなどを利用すればこの問題は解決できます。Sporkは(普通)変更されないコントローラ、モデル、ビュー、 factoryと変更が頻繁なほぼすべてのコードをプリロードします。
ここにSpork用のspec helperと Guardfile設定があります。この設定を使用すればプリロードされるファイル(initializerとか)が更新されたときのみアプリ全体をリロードして、単一テストをとても早く実行できます。
Sporkを使う際の欠点は、コードに積極的にモンキーパッチをあてているため、ファイルがリロードされない原因を理解するのに時間を無駄使いする可能性があることです。 もしSpinもしくは他の解決策があったら教えてください。
Zeusを使うためのGuardfile設定ファイルはここにあります。 spec_helperを修正する必要はありませんが、テストを実行する前にコンソールで`zeus start`をする必要があります。
全てにおいてZeusはSporkより比較的に安全な方法を使います。短所は使用要件がかなり厳しいことです。 Ruby 1.9.3以上(backported GCが使えるRuby 2.0以上を推奨) FSEventsもしくはinotifyが使えるOSを必要とします。
多くの人達がSporkから他の解決策に移行しました。この解決策はもっと良い設計でもっと良い答えが出せますし、意識的に必要なものだけロードするようにしています。 詳しいことは関連議論をご覧ください。
HTTP requestをmockする
テストする時、外部サービスに依存する必要がある場合がまれにあります。こんな場合実際サービスに依存させなくwebmockなどを使って stubするのが望ましいです。
good
context "with unauthorized access" do
let(:uri) { 'http://api.lelylan.com/types' }
before { stub_request(:get, uri).to_return(status: 401, body: fixture('401.json')) }
it "gets a not authorized notification" do
page.driver.get uri
expect(page).to have_content 'Access denied'
end
end
有用なformatter
formatterを使うとテストに関する有用な情報を得られます。個人的にはfuubarが大変良かったです。実行するにはgemを設置しGuardfileにfuubarをデフォルトのformatterとして設定する必要があります。
good
# Gemfile
group :development, :test do
gem 'fuubar'
good
# Guardfile
guard 'rspec' do
# ...
end
good
# .rspec
--drb
--format Fuubar
--color
fuubarに対してもっと学ぶ。
Books
<%= render "partials/books" %>Presentations
Resources on the web
<%= render "partials/links" %>Screencasts
<%= render "partials/screencasts" %>Libraries (documentation)
<%= render "partials/libraries" %>Styleguide
We are seeking for the best guidelines to write "nice to read" specs. Right now a good starting point is for sure the Mongoid test suite. It uses a clean style and easy-to-read specs, following most of the guidelines described here.
Improving Better Specs
This is an open source project. If something is missing or incorrect just file an issue to discuss the topic. Also check the following issues:
- Multilanguage (file an issue if you want to translate this guide)
Credits
The document was started by Andrea Reginato. A special thanks to the Lelylan Team. This document is licensed under MIT License.
Help us
If you have found those tips useful to improve your daily job think about making a $9 donation. Any donations will be used to make this site a more complete reference for better testing in Ruby.