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