LoginSignup
63
58

More than 5 years have passed since last update.

RSpec 3.0から3.3まで一気に上げたので独断と偏見で選んだ注目すべき新機能を紹介する

Last updated at Posted at 2015-06-23

実際にはほとんど3.3の話ですが、まぁ3.3それだけよさそうなんで、みなさん使ってみてください。

まずはRSpecのブログにも書いてあったものから。

(3.2での変更) Core: Performance Improvements

オブジェクトを割り当てる回数を減らして速くしたそうです。RSpecが速すぎて困る人は誰もいません。上げましょう。

(3.2での変更) Mocks: Mismatched Args Are Now Diffed

expect(x).to eq(y) して失敗した時と同じように、expect(x).to receive(:method_name).with(arg)と書いて失敗した時も予測した結果と実際の結果のdiffが出るようになりました。

(3.3での変更) Core: New --only-failures option

RSpec.configure do |c|
  c.example_status_persistence_file_path = "./tmp/rspec-example-status.txt"
end

などとspec_helper.rbに書いておくと、rspecを実行するたび./tmp/rspec-example-status.txtに最後に実行したexampleの状態(ちゃんと中身見てませんが成功したとか失敗したとかの情報でしょう)を記録してくれるそうです。
そして--only-failuresオプションを使用すると、名前通りこのファイルを見て、「最後にrspecを実行した時に失敗したexampleのみ実行」してくれるようになりました。素晴らしい! :smile:

(3.3での変更) Core: New --next-failure option

先ほど紹介した./tmp/rspec-example-status.txtに記録した情報を利用して、「最後にrspecを実行した時に失敗したexampleのうち、最初に失敗するexampleまで実行する」というオプションです。
--only-failures --fail-fast --order definedと等価なのだそうです。

(3.3での変更) Expectations: New aggregate_failures API

※この節のサンプルコードは一部 http://rspec.info/blog/2015/06/rspec-3-3-has-been-released/ から引用しています。

例えば :point_down: のように書くと

RSpec.describe Client do
  let(:response) { Client.make_request }

  it "returns a successful JSON response" do
    expect(response.status).to eq(200)
    expect(response.headers).to include("Content-Type" => "application/json")
    expect(response.body).to eq('{"message":"Success"}')
  end
end

example it "returns a successful JSON response" の、各expectationを順番に実行して、1つでもexpectationが失敗した場合、exampleは失敗したとみなされ、実行を中断します。

ところが、下記のようにaggregate_failuresとタグを指定すると...。

RSpec.describe Client do
  let(:response) { Client.make_request }

  it "returns a successful JSON response", :aggregate_failures do
    expect(response.status).to eq(200)
    expect(response.headers).to include("Content-Type" => "application/json")
    expect(response.body).to eq('{"message":"Success"}')
  end
end

各expectationを順番に実行してexpectationが失敗した場合でも、実行を中断しないようになります(もちろんexample自体は失敗したことになります)。

これによって、こういうケースでitブロックをちまちま分ける必要がなくなり、結果beforeブロックを実行する回数が減り、実行速度を高めたり、より簡潔にexampleを書けるようになります。
これはbeforeブロックでDBのレコードをたっぷり追加する、みたいなことをしている場合特に大きなメリットとなるでしょう。

しかし、名前がいかにもRSpecらしくない。言わなくても最初からそうしてほしいなぁ、
... と、思ったところ、下記のように設定すればデフォルトでそうできるんだとか。

RSpec.configure do |c|
  c.define_derived_metadata do |meta|
    meta[:aggregate_failures] = true unless meta.key?(:aggregate_failures)
  end
end

もしこの設定をした上で「aggregate_failuresしたくない!」と思った時は先ほどのようにtagに:aggregate_failuresと書く代わりに

# ここに書けばこのブロック内すべてのexampleが`aggregate_failures`されます。
RSpec.describe Client, aggregate_failures: false do
  let(:response) { Client.make_request }

  #                                        もちろん個別に指定してもOK!
  it "returns a successful JSON response", aggregate_failures: false do
    expect(response.status).to eq(200)
    expect(response.headers).to include("Content-Type" => "application/json")
    expect(response.body).to eq('{"message":"Success"}')
  end
end

と書きましょう。

(3.3での変更) Expectations: Improved Failure Output

色々良くなったようです。特にTimeオブジェクトが詳細に表示されるようになったのが大きいかな?
気づいた時は喜び、そして感謝しましょう。

ChangeLogも流し読みしてみたよ!

で、個人的にめちゃめちゃ大きかったのが以下の2つ。

  • Warn when using a bare raise_error matcher that you may be subject to false positives. (Jon Rowe, #768)

:point_down: こういう風に、引数なしでraise_errorを使用すると、警告が出るようになりました。

expect { subject_method }.to raise_error

このようにraise_errorの引数に何も指定しない場合、ブロックの中でどんなエラーが起きようとexampleが成功してしまいます。
例えばブロックの中で実行しているsubject_methodの名前が実は間違っていて本当はsubject_functionだったとしましょう。
それでもNoMethodErrorが発生するので、このexampleは成功してしまうのです。
どんなエラーでも成功する、ということにすると、意図通り実装ができていないのに成功してしまうexampleを書いてしまう危険性が非常に高いのです。
こうした弊害を防ぐため、警告を出すようになりました。

これ、関係するIssueを個人的にwatchしていたほど気になっていた問題であったため、修正されたのは非常にありがたいです。
ただ、個人的な事情を申しますと、CircleCIにしてから警告をあまり見なくなってしまったため、その習慣を改めないといけませんね...。

  • Ensure expectations that raise eagerly also raise during RSpec verification. This means that if exceptions are caught inside test execution the test will still fail. (Sam Phippen, #884)

:point_down: 例えばこういう実装で、

def just_hello
  puts "hello"
  frined.accept_hello_of(self)
rescue Exception
  warn "I JUST WANT TO SAY HELLO!! DON'T DISTURB!!"
end

:point_down: こういうexampleだった場合、

it 'says hello to no other person' do
  expect(friend).not_to receive(:accept_hello_of)
  x.just_hello
end

RSpec 3.3より前までは、expectationに反してfriendaccept_hello_ofしているにも関わらず、テストが通ってしまっていました。
なぜでしょう :question:

just_helloの実装のfriend.accept_hello_of(self)が呼ばれた時点でrspec-mocksの例外が発生するも、just_helloの中でrescue Exceptionしているため、その例外までrescueされ、無視されてしまうからです。

RSpec 3.3からは、こうしたケースに対応するため、rspec-mocksが例外を発生させたとしても、exampleの実行終了後、mockのverifyをしたタイミングで必ず失敗するよう修正されました。

これによって、嬉しい弊害が発生しました。
どういうわけか我々のコードではこのrescue Exceptionが多用されており、お陰でRSpec 3.3に上げた途端落ちるようになったexampleが17件もあったのです。
みなさんも移行の際はその辺を念頭に置きつつ、rescue Exceptionにはくれぐれも気をつけましょう。
安易に使ってはいけません。 :warning:

63
58
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
63
58