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

Rspec expectation文法に対するもっと詳しい情報はここここをご覧ください。

このガイドに関して議論する →

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 { allow(Resource).to receive(:where).with(created_from: params[:id]).and_return(false) }
  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
この例は基本的なリロードルールがあるGuardfileです。

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の設定があります。

guard-rspec

このガイドに関して議論する →

sporkを使ったテスト高速化

Railsでテストを実行するには全体Railsのロードが必要です。これは時間がかかり仕事のじゃまになり得ます。 ZeusSpinSporkなどを利用すればこの問題は解決できます。Sporkは(普通)変更されないコントローラ、モデル、ビュー、 factoryと変更が頻繁なほぼすべてのコードをプリロードします。

ここにSpork用のspec helperGuardfile設定があります。この設定を使用すればプリロードされるファイル(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

webmockVCRに対してもっと学ぶ。 二つを一緒に使う方法を説明した 良いプレゼン もあります。

このガイドに関して議論する →

有用な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に対してもっと学ぶ。

このガイドに関して議論する →

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)

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.






guard-rspecに対してもっと学ぶ。