Movatterモバイル変換


[0]ホーム

URL:


RSpec 3.3 has been released!

Myron Marston

Jun 12, 2015

RSpec 3.3 has just been released! Given our commitment tosemantic versioning, this should be a trivialupgrade for anyone already using RSpec 3.0, 3.1 or 3.2, but if we did introduceany regressions, please let us know, and we’ll get a patch releaseout with a fix ASAP.

RSpec continues to be a community-driven project with contributorsfrom all over the world. This release includes 769 commits and 200merged pull requests from nearly 50 different contributors!

Thank you to everyone who helped make this release happen!

Notable Changes

Core: Unique IDs for every Example and Example Group

Historically, RSpec examples have been identified primarily byfilelocation. For example, this command:

$ rspec spec/unit/baseball_spec.rb:23

…would run an example or example group defined on line 23 ofspec/unit/baseball_spec.rb. Location-based identification generallyworks well, but does not always uniquely identity a particular example.For example, if you use shared examples, your spec suite mayhave multiple copies of the example defined atspec/support/shared_examples.rb:47.

RSpec 3.3 introduces a new way to identify examples and example groups:unique IDs. The IDs are scoped to a particular file and are based on theindex of the example or group. For example, this command:

$ rspec spec/unit/baseball_spec.rb[1:2,1:4]

…would run the 2nd and 4th example or group defined under the 1sttop-level group defined inspec/unit/baseball_spec.rb.

For the most part, the new example IDs are primarily used internally byRSpec to support some of the new 3.3 features, but you’re free to usethem from the command line. The re-run command printed by RSpec foreach failure will use them if the file location does not uniquelyidentify the failed example. Copying and pasting the re-run commandfor a particular failed example will always run just that example now!

Core: New--only-failures option

Now that RSpec has a robust way to uniquely identify every example, we’veadded new filtering capabilities to allow you to run only the failures. Toenable this feature, you first have to configure RSpec so it knows whereto persist the status of each example:

RSpec.configuredo|c|c.example_status_persistence_file_path="./spec/examples.txt"end

Once you’ve configured that, RSpec will begin persisting the status of eachexample after every run of your suite. You’ll probably want to add this fileto.gitignore (or whatever the equivalent for your source control system is),as it’s not intended to be kept under source control. With that configurationin place, you can use the new CLI option:

$ rspec --only-failures# or apply it to a specific file or directory:$ rspec spec/unit --only-failures

It’s worth noting that this option filters to the examples that failedthe last time they ran, not just to the failures from the last run ofrspec. That means, for example, that if there’s a slow acceptance specthat you don’t generally run locally (leaving it to your CI server torun it), and it failed the last time it ran locally, even if it was weeks ago,it’ll be included when you runrspec --only-failures.

See ourrelishdocsfor an end-to-end example of this feature.

Core: New--next-failure option

When making a change that causes many failures across my spec suite—suchas renaming a commonly used method—I’ve often used a specific work flow:

  1. Run the entire suite to get the failure list.
  2. Run each failure individually in sequence using the re-run commandthat RSpec printed, fixing each example before moving on to the next failure.

This allows me to systematically work through all of the failures,without paying the cost of repeatedly running the entire suite. RSpec3.3 includes a new option that vastly simplifies this work flow:

$ rspec --next-failure# or apply it to a specific file or directory:$ rspec spec/unit --next-failure

This option is the equivalent of--only-failures --fail-fast --order defined.It filters to only failures, and will abort as soon as one fails. Itapplies--order defined in order to ensure that you keep getting the samefailing example when you run it multiple times in a row without fixingthe example.

Core: Stable Random Ordering

RSpec’s random ordering has always allowed you to pass a particular--seed to run the suite in the same order as a prior run. However,this only worked if you ran the same set of examples as the originalrun. If you apply the seed to a subset of examples, their orderingwouldn’t necessarily be consistent relative to each other. This isa consequence of howArray#shuffle works:%w[ a b c d ].shuffle mayorderc beforeb, but%w[ b c d ].shuffle may ordercafterb even if you use the same random seed.

This may not seem like a big deal, but it makes--seed far lessuseful. When you are trying to track down the source of an orderingdependency, you have to keep running the entire suite to reproducethe failure.

RSpec 3.3 addresses this. We no longer useshuffle for randomordering. Instead, we combine the--seed value with the id of eachexample, hash it, and sort by the produced values. This ensures thatif a particular seed orders examplec before exampleb,c willalways come beforeb when you re-use that seed no matter what subsetof the suite you are running.

While stable random ordering is useful in its own right, the big winhere is a new feature that it enables: bisect.

Core: Bisect

RSpec has supported random ordering (with a--seed option to reproducea particular ordering) since RSpec 2.8. These features help surfaceordering dependencies between specs, which you’ll want to quicklyisolate and fix.

Unfortunately, RSpec provided little to help with isolatingan ordering dependency. That’s changing in RSpec 3.3. We now providea--bisect option that narrows an ordering dependency down to aminimal reproduction case. The new bisect flag repeatedly runs subsetsof your suite in order to isolate the minimal set of examples thatreproduce the same failures when you run your whole suite.

Stable random ordering makes it possible for you to run various subsetsof the suite to try to narrow an ordering dependency down to a minimal reproduction case.

See ourrelishdocsfor an end-to-end example of this feature.

Core: Thread Safelet andsubject

Historically,let andsubject havenever beenthread safe.That’s changing in RSpec 3.3, thanks to thegreatwork of Josh Cheek.

Note that the thread safety synchronization does add a bit of overhead,as you’d expect. If you’re not spinning up any threads in your examplesand want to avoid that overhead, you can configure RSpec todisable the thread safety.

Expectations: Newaggregate_failures API

When you’ve got multiple independent expectations to make about aparticular result, there’s generally two routes you can take. One way isto define a separate example for each expectation:

RSpec.describeClientdolet(:response){Client.make_request}it"returns a 200 response"doexpect(response.status).toeq(200)endit"indicates the response body is JSON"doexpect(response.headers).toinclude("Content-Type"=>"application/json")endit"returns a success message"doexpect(response.body).toeq('{"message":"Success"}')endend

This follows the “one expectation per example” guideline that encouragesyou to keep each spec focused on a single facet of behavior. Alternately,you can put all the expectations in a single example:

RSpec.describeClientdoit"returns a successful JSON response"doresponse=Client.make_requestexpect(response.status).toeq(200)expect(response.headers).toinclude("Content-Type"=>"application/json")expect(response.body).toeq('{"message":"Success"}')endend

This latter approach is going to be faster, as the request is only made oncerather than three times. However, if the status code is not a 200, the examplewill abort on the first expectation and you won’t be able to see whether or notthe latter two expectations passed or not.

RSpec 3.3 has a new feature that helps when, for speed or other reasons, youwant to put multiple expectations in a single example. Simply wrap yourexpectations in anaggregate_failures block:

RSpec.describeClientdoit"returns a successful JSON response"doresponse=Client.make_requestaggregate_failures"testing response"doexpect(response.status).toeq(200)expect(response.headers).toinclude("Content-Type"=>"application/json")expect(response.body).toeq('{"message":"Success"}')endendend

Within theaggregate_failures block, expectations failures do not cause theexample to abort. Instead, a singleaggregate exception will beraised at the end containing multiple sub-failures which RSpec willformat nicely for you:

1) Client returns a successful response   Got 3 failures from failure aggregation block "testing reponse".   # ./spec/client_spec.rb:5   1.1) Failure/Error: expect(response.status).to eq(200)          expected: 200               got: 404          (compared using ==)        # ./spec/client_spec.rb:6   1.2) Failure/Error: expect(response.headers).to include("Content-Type" => "application/json")          expected {"Content-Type" => "text/plain"} to include {"Content-Type" => "application/json"}          Diff:          @@ -1,2 +1,2 @@          -[{"Content-Type"=>"application/json"}]          +"Content-Type" => "text/plain",        # ./spec/client_spec.rb:7   1.3) Failure/Error: expect(response.body).to eq('{"message":"Success"}')          expected: "{\"message\":\"Success\"}"               got: "Not Found"          (compared using ==)        # ./spec/client_spec.rb:8

RSpec::Core provides improved support for this feature through the use ofmetadata. Instead of wrapping the expectations withaggregate_failures,simply tag the example or group with:aggregate_failures:

RSpec.describeClient,:aggregate_failuresdoit"returns a successful JSON response"doresponse=Client.make_requestexpect(response.status).toeq(200)expect(response.headers).toinclude("Content-Type"=>"application/json")expect(response.body).toeq('{"message":"Success"}')endend

If you want to enable this feature everywhere, you can usedefine_derived_metadata:

RSpec.configuredo|c|c.define_derived_metadatado|meta|meta[:aggregate_failures]=trueunlessmeta.key?(:aggregate_failures)endend

Of course, you may not want this enabled by default everywhere. Theunless meta.key?(:aggregate_failures) bit allows you to opt out individualexamples or groups by tagging them withaggregate_failures: false. Whenyou’ve gotdependent expectations (e.g. where an expectation only makessense if the prior expectation passed), or if you’re using expectations toexpress a pre-condition, you’ll probably want the example to immediately abortwhen the expectation fails.

Expectations: Improved Failure Output

RSpec 3.3 includes improved failure messages across the board for all matchers.Test doubles now look prettier in our failure messages:

Failure/Error: expect([foo]).to include(bar)  expected [#<Double "Foo">] to include #<Double "Bar">  Diff:  @@ -1,2 +1,2 @@  -[#<Double "Bar">]  +[#<Double "Foo">]

In addition, RSpec’s improved formatting forTime and other objects willnow be used wherever those objects are inspected, regardless of whichbuilt-in matcher you used. So, for example, where you used to get this:

Failure/Error: expect([Time.now]).to include(Time.now)  expected [2015-06-09 07:48:06 -0700] to include 2015-06-09 07:48:06 -0700

…you’ll now get this instead:

Failure/Error: expect([Time.now]).to include(Time.now)  expected [2015-06-09 07:49:16.610635000 -0700] to include 2015-06-09 07:49:16.610644000 -0700  Diff:  @@ -1,2 +1,2 @@  -[2015-06-09 07:49:16.610644000 -0700]  +[2015-06-09 07:49:16.610635000 -0700]

…which makes it much clearer that the time objects differ at the subsecond level.

Thanks to Gavin Miller, Nicholas Chmielewski and Siva Gollapalli for contributing tothese improvements!

Mocks: Improved Failure Output

RSpec::Mocks has also received some nice improvements to its failure output. RSpec’simproved formatting forTime and other objects is now applied to mock expectationfailures as well:

Failure/Error: dbl.foo(Time.now)  #<Double (anonymous)> received :foo with unexpected arguments    expected: (2015-06-09 08:33:36.865827000 -0700)         got: (2015-06-09 08:33:36.874197000 -0700)  Diff:  @@ -1,2 +1,2 @@  -[2015-06-09 08:33:36.865827000 -0700]  +[2015-06-09 08:33:36.874197000 -0700]

In addition, the failure output forhave_received has been much improved so that whenthe expected args do not match, it lists each set of actual args, and the number oftimes the message was received with those args:

Failure/Error: expect(dbl).to have_received(:foo).with(3)  #<Double (anonymous)> received :foo with unexpected arguments    expected: (3)         got: (1) (2 times)              (2) (1 time)

Thanks to John Ceh for implementing the latter improvement!

Mocks: StubbingMyClass.new Verifies AgainstMyClass#initialize

RSpec’sverifying doublesuse the metadata that Ruby’s reflection capabilities provide to verify,among other things, that passed arguments are valid according to theoriginal method’s signature. However, when a method is defined usingjust an arg splat:

defmethod_1(*args)method_2(*args)enddefmethod_2(a,b)end

…then the verifying double is going to allowany arguments, evenif the method simply delegates toanother method that does have astrong signature. Unfortunately,Class#new is one of these methods.It’s defined by Ruby to delegate to#initialize, and will only acceptarguments that the signature ofinitialize can handle, but the metadataprovided byMyClass.method(:new).parameters indicates it can handle anyarguments, even if it can’t.

In RSpec 3.3, we’ve improved verifying doubles so that when you stubnew on a class, it uses the method signature of#initialize toverify arguments, unless you’ve redefinednew to do somethingdifferent. This allows verifying doubles to give you an error whenyou pass arguments to a stubbedMyClass#new method that the realclass would not allow.

Rails: Generated scaffold routing specs now include PATCH spec

Rails 4 added support forPATCH as the primary HTTP method forupdates. The routing matchers in rspec-rails have likewise supportedPATCH since 2.14, but the generated scaffold spec were not updatedto match. This has beenaddressed inrspec-rails 3.3.

Thanks to Igor Zubkov for this improvement!

Rails: New:job spec type

Now thatActiveJob is part of Rails, rspec-rails has a new:job spectype that you can opt into by either tagging your example group with:type => :job or putting the spec file inspec/jobs if you’veenabledinfer_spec_type_from_file_location!.

Thanks to Gabe Martin-Dempesy for this improvement!

Stats

Combined:

rspec-core:

rspec-expectations:

rspec-mocks:

rspec-rails:

rspec-support:

Docs

API Docs

Cucumber Features

Release Notes

rspec-core-3.3.0

Full Changelog

Enhancements:

Bug Fixes:

rspec-expectations-3.3.0

Full Changelog

Enhancements:

Bug Fixes:

rspec-mocks-3.3.0

Full Changelog

Enhancements:

Bug Fixes:

rspec-rails-3.3.0

Full Changelog

Enhancements:

rspec-support-3.3.0

Full Changelog

Enhancements:

Bug Fixes:


[8]ページ先頭

©2009-2025 Movatter.jp