RSpec 3.4 がリリースされました!

Yuji Nakayama

Nov 13, 2015

RSpec 3.4 がリリースされました! 私たちは semantic versioning に準拠する方針を掲げているため、 このリリースはすでに RSpec 3 を使っている方にとってなにか対応が必要になるものではありません。 しかし、もし私たちがバグを作り込んでしまっていた場合は教えてください。 できるだけ早く修正をし、パッチ版をリリースします。

RSpec は世界中のコントリビュータと共に、コミュニティ主導のプロジェクトであり続けます。 今回のリリースには、50 人近くのコントリビュータによる 500 以上のコミットと 160 以上の pull request が含まれています!

このリリースに向けて力になってくれたみなさん、ありがとう!

主要な変更

Core: Bisect アルゴリズムの改善

RSpec 3.3 では、 実行順序依存の失敗の原因を探す上で、 失敗を再現する最小限のコマンドを特定するための --bisect オプションを導入しました。 このときの二分アルゴリズムは単純な並び替えによる方法であり、 各ラウンドごとに、 まず最初の半分の example 群を試し、次にもう一方の半分、そしてそれら example 群の半分ずつの各組み合わせ、 という流れを、確実に無視できる半分を見つけるまで繰り返すものでした。 この方法は大抵の場合は問題ありませんでしたが、最悪のケースではひどい挙動を示すことがありました。 具体的には、実行順序依存の原因として 複数の example が関わっていた場合、 それら原因の example 群をすべて含んだ多数の組み合わせが発生してしまうことがありました。 さらに、残り半分以上の example 群が原因だった場合はすべての組み合わせを網羅的に試そうとし、 非常に長時間かかってしまうこともありました。

RSpec 3.4 では、この二分アルゴリズムは はるかに 賢くなりました。 新しいアルゴリズムは再帰ベースになり、最小の試行回数で原因を特定します。 この新しいアルゴリズムはすでに非常に良い反響を得ており、 Sam Livingston-Gray は 3.3 の二分アルゴリズムは一晩中かかっても終わらないと報告していましたが、 新しいアルゴリズムでは 20 分足らずで完了したとのことです!

これを実装してくれた Simon Coffey、ありがとう! もしこのアルゴリズムについてもっと知りたい場合は、 こちらの pull request を参照してください。 アルゴリズムの理解に役立つ図も載っています。

Core: 失敗時の出力の改善

RSpec は失敗時のわかりやすいログ出力をこれまでずっと重要視してきましたが、 3.4 では様々な方法によってさらに改善されました。

複数行のコードスニペット

RSpec は、エクスペクテーションが失敗したときにそのコードスニペットを表示します。 RSpec 3.4 以前では、エクスペクテーションが1行に収まっている場合は問題ありませんでしたが、 以下のように複数行で記述した場合、

expect {
  MyNamespace::MyClass.some_long_method_name(:with, :some, :arguments)
}.to raise_error(/some error snippet/)

最初の1行目(expect {)しか表示されませんでした。 なぜなら例外オブジェクトには、スタックフレームとしてそれだけの情報しか含まれていないからです。 RSpec 3.4 では、 標準ライブラリの Ripper が利用可能な場合はそれを読み込み、 ソースをパースして問題のエクスペクテーションの式が何行続くかを判断するようになりました。 上記のような複数行のケースでは、式全体が表示されるようになります。

また、これに伴う設定オプション config.max_displayed_failure_line_count も追加されており、 表示されるスニペットの最大行数を設定することができます(デフォルトで 10)。

これを実装してくれた Yuji Nakayama、ありがとう!

coderayがインストール済みの場合、シンタックスハイライトが有効に

これをさらに一歩進めて、coderay gem がインストールされている場合、 RSpec 3.4 はコードスニペットのシンタックスハイライトを行うようになりました。 前述のスニペットの場合、こんな感じで表示されます。

Failure with syntax highlighting

失敗元の行の検出の改善

RSpec は、例外のスタックトレース中から適切なフレームを調べることで、失敗の元となったコードスニペットを探し出します。 これを行う上で、単純にスタックトレースの一番上のフレームを使うこともできますが、 それは大抵の場合あなたが求めているものではありません。 例えばエクスペクテーションが失敗した場合は、 一番上のフレームは常に RSpec 内の RSpec::Expectations::ExpectationNotMetError が発生した箇所になりますが、 あなたが知りたいのは RSpec 内のコードではなく、spec ファイル中で expect を呼び出している箇所でしょう。 RSpec 3.4 以前のこの実装はかなり単純で、 現在実行中の example が含まれている spec ファイル内の一番最初のスタックフレームを探すだけでした。 そのため、場合によっては間違ったスニペットを表示してしまうことがありました (例えば spec ファイル内から、spec/support 以下で定義されたヘルパーメソッドを呼び出しており、本当はそこで失敗していた場合)。 また、該当するスタックフレームを見つけられなかった場合は Unable to find matching line from backtrace と表示するしかありませんでした。

RSpec 3.4 ではこのロジックが改善され、 まずは config.project_source_dirs(デフォルトで libappspec)に含まれる最初のフレームを探し、 もし該当するフレームが見つからなかった場合は一番最初のスタックフレームにフォールバックします。 もう Unable to find matching line from backtrace が表示されることはありません!

Expectations: 複合エクスペクテーションの失敗時メッセージの改善

さらに失敗時出力の改善が続きます。 rspec-expectations 3.4 では、複合エクスペクテーション(compound expectations)の失敗メッセージが改善されました。 これまでは複数の失敗メッセージを単純に1行に連結しており、例えば以下のようなエクスペクテーションの場合、

expect(lyrics).to start_with("There must be some kind of way out of here")
              .and include("No reason to get excited")

このような読みにくいメッセージが出力されてしまっていました。

1) All Along the Watchtower has the expected lyrics
   Failure/Error: expect(lyrics).to start_with("There must be some kind of way out of here")
     expected "I stand up next to a mountain And I chop it down with the edge of my hand" to start with "There must be some kind of way out of here" and expected "I stand up next to a mountain And I chop it down with the edge of my hand" to include "No reason to get excited"
   # ./spec/example_spec.rb:20:in `block (2 levels) in <top (required)>'

RSpec 3.4 では、それぞれのメッセージが個別に表示されるようになり、読みやすくなりました。

1) All Along the Watchtower has the expected lyrics
   Failure/Error:
     expect(lyrics).to start_with("There must be some kind of way out of here")
                   .and include("No reason to get excited")

        expected "I stand up next to a mountain And I chop it down with the edge of my hand" to start with "There must be some kind of way out of here"

     ...and:

        expected "I stand up next to a mountain And I chop it down with the edge of my hand" to include "No reason to get excited"
   # ./spec/example_spec.rb:20:in `block (2 levels) in <top (required)>'

Expectations: match マッチャへの with_captures の追加

RSpec 3.4 では、match マッチャに新しい機能が追加され、正規表現のキャプチャを指定することができるようになりました。 新しい with_captures メソッドを使って、このようにインデックスベースのキャプチャを指定することができます。

year_regex = /(\d{4})\-(\d{2})\-(\d{2})/
expect(year_regex).to match("2015-12-25").with_captures("2015", "12", "25")

また、名前付きキャプチャを指定することも可能です。

year_regex = /(?<year>\d{4})\-(?<month>\d{2})\-(?<day>\d{2})/
expect(year_regex).to match("2015-12-25").with_captures(
  year: "2015",
  month: "12",
  day: "25"
)

Sam Phippen と、この実装にあたって協力してくれた Jason Karns、ありがとう。

Rails: ActiveJob のための新しい have_enqueued_job マッチャ

Rails 4.2 には ActiveJob が組み込まれました。 rspec-rails 3.4 では、任意のコードがジョブをキューに加えることを指定するためのマッチャが追加されました。 このマッチャはメソッドチェーンによるインターフェースを持っており、 rspec-mock を使ったことがあれば見覚えがあるのではないでしょうか。

expect {
  HeavyLiftingJob.perform_later
}.to have_enqueued_job

expect {
  HelloJob.perform_later
  HeavyLiftingJob.perform_later
}.to have_enqueued_job(HelloJob).exactly(:once)

expect {
  HelloJob.perform_later
  HelloJob.perform_later
  HelloJob.perform_later
}.to have_enqueued_job(HelloJob).at_least(2).times

expect {
  HelloJob.perform_later
}.to have_enqueued_job(HelloJob).at_most(:twice)

expect {
  HelloJob.perform_later
  HeavyLiftingJob.perform_later
}.to have_enqueued_job(HelloJob).and have_enqueued_job(HeavyLiftingJob)

expect {
  HelloJob.set(wait_until: Date.tomorrow.noon, queue: "low").perform_later(42)
}.to have_enqueued_job.with(42).on_queue("low").at(Date.tomorrow.noon)

この機能を実装してくれた Wojciech Wnętrzak、ありがとう!

統計

全体:

rspec-core:

rspec-expectations:

rspec-mocks:

rspec-rails:

rspec-support:

ドキュメント

API ドキュメント

Cucumber フィーチャ

リリースノート

rspec-core 3.4.0

すべての Changelog

改善:

バグ修正:

rspec-expectations 3.4.0

すべての Changelog

改善:

バグ修正:

rspec-mocks 3.4.0

すべての Changelog

改善:

バグ修正:

rspec-rails 3.4.0

Full Changelog

改善:

バグ修正:

rspec-support 3.4.0

すべての Changelog

改善:

バグ修正: