RSpecテクニック
共通処理の切り出し
spec/rails_helper.rb
ファイルの最下部に以下のコードを追加する。
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
spec/support/
ディレクトリに共通処理などを切り出したファイルを作成する。
例1:shared_examplesを切り出す。
# spec/support/ファイル名.rb
RSpec.shared_examples 'shared_examples名' do
# ...
end
例2: Concernを切り出す。
# spec/support/ファイル名.rb
module ConcernName
extend ActiveSupport::Concern
included do
let(:name) { '...' }
end
# spec内で呼び出せるメソッド
def method
# ...
end
end
# 特定のSpecに事前に適用させる
# この例はmodelのspecに適用させる
RSpec.configure do |config|
config.include ConcernName, type: :model
end
以下のようにすれば、describe単位でConcernの適用をスイッチできる。
RSpec.configure do |config|
config.include ConcernName, :some_name
end
describe '説明', :some_name do
# include ConcernNameを実行した状態になる
end
https://www.rubydoc.info/github/rspec/rspec-core/RSpec%2FCore%2FConfiguration:includeを参照。
必要なファイルのみロードする
「共通処理の切り出し」の通りにすると1つのSpecファイルを実行するだけでも全てのファイルがrequire
されることになる。when_first_matching_example_defined
を使うと指定したタグ(メタデータ)にマッチするSpecが実行されたときに初めてファイルをrequire
することができる。
RSpec.configure do |config|
config.when_first_matching_example_defined(type: :system) do
require 'support/system_concern'
config.include SystemConcern, type: :system
end
end
RSpec.describe 'シナリオ', type: :system do
# 以下を実行したのと同じ状態になる
# require 'support/system_concern'
# include SystemConcern
end
複数のexpect
を持つit
で失敗するexpect
があってもexpect
を全て実行させる
以下のようなit
がある場合、1番目のexpect
が失敗した時点で切り上げられて2番目以降のexpect
は実行されない。
it do
expect(1).to eq 2
expect(2).to eq 2 # 実行されない
expect(3).to eq 2 # 実行されない
end
aggregate_failures
を使うと全てのexpect
が実行されるようになる。
it do
aggregate_failures '集約' do
expect(1).to eq 2
expect(2).to eq 2 # 実行される
expect(3).to eq 2 # 実行されて失敗が報告される
end
end
it
にメタデータとして:aggregate_failures
を指定する方法もある。
it 'spec', :aggregate_failures do
expect(1).to eq 2
expect(2).to eq 2 # 実行される
expect(3).to eq 2 # 実行されて失敗が報告される
end
グローバルに適用するのであれば、spec_helper.rb
に以下を追記する。
config.define_derived_metadata do |meta|
meta[:aggregate_failures] = true
end
マッチャーの自作
以下のようなマッチャーを自作でき、expect([1,2,3]).to average '2'
のように使えるようになる。
RSpec::Matchers.define :average do |expected|
# 平均値を計算するヘルパーメソッド
def average(actual)
actual.sum(&:to_d) / actual.length
end
# マッチャーの本体
# これだけ必須
match do |actual|
average(actual) == expected.to_d
rescue StandardError
false
end
# `expect(...).to average '...'`の失敗時のメッセージ
failure_message do |actual|
begin
result = "#{average(actual)}だった"
rescue StandardError => e
result = "#{e.class}(#{e.message})が発生した"
end
"#{actual}の平均値は#{expected}と期待したが#{result}"
end
# `expect(...).not_to average '...'`の失敗時のメッセージ
failure_message_when_negated do |actual|
begin
result = "#{average(actual)}だった"
rescue StandardError => e
result = "#{e.class}(#{e.message})が発生した"
end
"#{actual}の平均値は#{expected}ではないと期待したが#{result}"
end
# -f docオプションを指定した時に出力される内容
description do
"平均値は#{expected}"
end
# not_to, to_not 時にmatchとは別の方法でテストさせたい場合に使う
# match_when_negated do |actual|
# expected.none? { |e| actual.include?(e) }
# end
end
change
マッチャーのようにブロックを受け取る場合はblock_arg.call
を呼ぶと指定されたブロックが実行される。expect
にブロックを渡したい場合はマッチャーの中でsupports_block_expectations
を呼んだ上で、match
のブロック引数を使ってactual.call
とすればよい。
# expect { ... }.to change { ... }.from(...).to(...)を再現したもの
RSpec::Matchers.define :my_change do
match do |actual|
break false unless block_arg.call == from_val
actual.call
block_arg.call == to_val
end
chain :from, :from_val
chain :to, :to_val
supports_block_expectations
end