RSpec / Ruby

もう1週間経ってしまいましたがRSpec3.1がリリースされたので勉強がてら主な新機能・変更点をまとめてみました。

なお、この記事はMyron Marstonの記事の要約になっています。

Backtraceフィルタの変更

RSpec3.0では表示していなかったgemの中のbacktraceをデフォルトで表示するようになっています。

非表示にする場合は以下を設定します。

RSpec.configure do |config|
  config.backtrace_exclusion_patterns << /gems/
end

特定のgemのbacktraceを非表示にすることもできます。

RSpec.configure do |config|
  config.filter_gems_from_backtrace "rack", "rake"
end

–exclude-patternオプションの追加

$ rspec --pattern "spec/**/*_spec.rb" --exclude-pattern "spec/acceptance/**/*_spec.rb"

という感じで除外するspecファイルを指定できます。

Rakefileに書くとこうなります。

require 'rspec/core/rake_task'

desc "Run all but the acceptance specs"
RSpec::Core::RakeTask.new(:all_but_acceptance) do |t|
  t.exclude_pattern = "spec/acceptance/**/*_spec.rb"
end

スタンドアロンでも設定なしで動く

RSpec3.0のデフォルト設定ではrspec-expectationやrspec-mocksがないとロード失敗でエラーになっていました。

RSpec3.1ではロードできなくてもエラーが起きないようになっていて、spec-coreだけで使いやすくなっています。

.rspecを–warningsフラグtrueで生成しない

RailsでWarningがたくさん出てしまうという理由で、Railsで.rspecを自動生成した時の--warningsのデフォルト値をtrueにしないようになっています。

have_attributesマッチャの追加

オブジェクトのattributeテストを簡単に行えるhave_attributesマッチャが追加されています。

Person = Struct.new(:name, :age)
person = Person.new("Alice", 20)
expect(person).to have_attributes(name: "Alice", age: 20)

このマッチャはan_object_having_attributesにもエイリアスされていて

people = [Person.new("Alice", 20), Person.new("Bob", 30)]
expect(people).to match([
  an_object_having_attributes(name: "Alice", age: 20),
  an_object_having_attributes(name: "Bob",   age: 30)
])

という使い方もできます。

メソッドへ渡るメッセージにも使えるようです。

expect(article).to receive(:author=).with(
  an_object_having_attributes(name: "Alice")
)

Compoundマッチャをブロックでも利用可能

以下のように1つのブロックに対して複数のマッチャを続けて書くことができます。

x = y = 0
expect {
  x += 1
  y += 2
}.to change { x }.to(1).and change { y }.to(2)

define_negated_matcherの追加

例えばexcludeincludeの反対として定義できます。

RSpec::Matchers.define_negated_matcher :exclude, :include

こうすると

expect(odd_numbers).not_to include(14)

の代わりに

expect(odd_numbers).to exclude(14)

と書けるようになります。

この機能はCompoundマッチャと組み合わせると非常に便利です。

adults = Town.find("Springfield").adults
marge  = Character.find("Marge")
bart   = Character.find("Bart")

expect(adults).to include(marge).and exclude(bart)

カスタムマッチャチェインをdescriptionにも出力

Spec::Matchers.define :be_smaller_than do |max|
  chain :and_bigger_than do |min|
    @min = min
  end

  match do |actual|
    actual < max && actual > @min
  end
end

# usage:
expect(10).to be_smaller_than(20).and_bigger_than(5)

のようなカスタムマッチャがあった場合、RSpec3.0だとこのようなメッセージが出力されていました。

Failure/Error: expect(5).to be_smaller_than(10).and_bigger_than(7)
  expected 5 to be smaller than 10

RSpec3.1では次のようになります。

Failure/Error: expect(5).to be_smaller_than(10).and_bigger_than(7)
  expected 5 to be smaller than 10 and bigger than 7

ただし、この機能は後方互換のためにデフォルトoffになっているので

RSpec.configure do |config|
  config.expect_with :rspec do |expectations|
    expectations.include_chain_clauses_in_custom_matcher_descriptions = true
  end
end

を設定する必要があります。

RSpec4からはデフォルトでonになるようです。

*_spyメソッドの追加

これまでdoubleを使う時にnilを返すだけのメソッドでも

spy = double(:foo => nil)
expect(spy).to have_received(:foo)

というようにメソッド名(:foo)を2回書かなければいけませんでした。

RSpec3.1ではas_null_objectメソッドが追加されて

spy = double.as_null_object
expect(spy).to have_received(:foo)

というようにメソッド名を1回だけ書けば済むようになっています。

また、as_null_objectをいちいち書かなくてもいいように下記のメソッドが用意されています。

spy(...)          # double(...).as_null_object と同じ
instance_spy(...) # instance_double(...).as_null_object  と同じ
class_spy(...)    # class_double(...).as_null_object と同じ
object_spy(...)   # object_double(...).as_null_object と同じ

and_wrap_originalの追加

and_wrap_originalを使うとテスト時にメソッドをラップできるようになります。

allow(api_client).to receive(:fetch_users).and_wrap_original do |original_method, *args|
  original_method.call(*args).first(10) # テスト時は10ユーザだけ取得する
end

Rails4.2のサポート

Rails3.1は公式にRails4.2をサポートするとのことです。

rails_helper.rbではspec/supportを自動ロードしない

spec/supportを全てロードすると時間がかかるため、自動生成されたrails_helper.rbではspec/supportを自動ロードしないようになっています。

RSpec3.1では必要なファイルだけロードすることが推奨されています。

まとめ

以上、RSpec3.1の新機能の紹介でした。

個人的にはdefine_negated_matcherが活躍してくれそうな気がしてます。