Notable Changes in RSpec 3
Myron Marston
May 21, 2014Update: there’s a Japanese translation of this available now.
RSpec 3.0.0 RC1 was released a couple days ago, and 3.0.0 final is just around the corner. We’ve been using the betas for the last 6 months and we’re excited to share them with you. Here’s whats new:
Across all gems
Removed support for Ruby 1.8.6 and 1.9.1
These versions of Ruby were end-of-lifed long ago and RSpec 3 does not support them.
Improved Ruby 2.x support
Recent releases of RSpec 2.x (i.e. those that came out after Ruby 2.0 was released) have officially supported Ruby 2, but RSpec 3’s support is greatly improved. We now provide support for working with the new features of Ruby 2, like keyword arguments and prepended modules.
New rspec-support gem
rspec-support is a new gem that we’re using for common code needed by more than one of rspec-(core|expectations|mocks|rails). It doesn’t currently contain any public APIs intended for use by end users or extension library authors, but we may make some of its APIs public in the future.
If you run bleeding-edge RSpec by sourcing it from github in your Gemfile, you’ll need to start doing the same for rspec-support as well.
Robust, well-tested upgrade process
Every breaking change in RSpec 3 has a corresponding deprecation warning in 2.99. Throughout the betas we have done many upgrades to ensure this process is as smooth as possible. We’ve put together step by step upgrade instructions.
The upgrade process also highlights RSpec’s new deprecation system which is highly configurable (allowing you to output deprecations into a file or turn all deprecations into errors) and is designed to minimize duplicated deprecation output.
Improved Docs
We’ve put a ton of effort into updating the API docs for all gems. They’re currently hosted on rubydoc.info:
…but we’re currently working on updating rspec.info to self-host them.
While the docs are still a work-in-progress (and frankly, always will be), we’ve made sure to explicitly declare all public APIs as part of SemVer compliance. We’re absolutely committed to maintaining all public APIs through all 3.x releases. Private APIs, on the other hand, are labeled as such because we specifically want to reserve the flexibility to change them willy nilly in any 3.x release.
Please do not use APIs we’ve declared private. If you find yourself with a need not addressed by the existing public APIs, please ask. We’ll gladly either make a private API public for your needs or add a new API to meet your use case.
Gems are now signed
We’ve started signing our gem releases. While the current gem signing system is far from ideal, and a better solution is being developed, it’s better than nothing. We’ve put our public cert on GitHub.
For more details on the current gem signing system, see A Practical Guide to Using Signed Ruby Gems.
Zero monkey patching mode
RSpec can now be used without any monkey patching whatsoever.
Much of the groundwork for this was laid in recent 2.x releases
that added the new expect
-based syntax to rspec-expectations
and rspec-mocks. We’ve gone the rest of the way in RSpec 3 and
provided alternatives for the remaining monkey patches.
For convenience you can disable all of the monkey patches with one option:
# spec/spec_helper.rb
RSpec.configure do |c|
c.disable_monkey_patching!
end
Thanks to Alexey Fedorov for implementing this config option.
For more info:
rspec-core
New names for hook scopes: :example
and :context
RSpec 2.x had three different hook scopes:
describe MyClass do
before(:each) { } # runs before each example in this group
before(:all) { } # runs once before the first example in this group
end
# spec/spec_helper.rb
RSpec.configure do |c|
c.before(:each) { } # runs before each example in the entire test suite
c.before(:all) { } # runs before the first example of each top-level group
c.before(:suite) { } # runs once after all spec files have been loaded, before the first spec runs
end
At times, users have expressed confusion around what :each
vs :all
means, and :all
in particular can be confusing when you use it in a
config block:
# spec/spec_helper.rb
RSpec.configure do |c|
c.before(:all) { }
end
In this context, the term :all
suggests that this hook will run once
before all examples in the suite — but that is what :suite
is for.
In RSpec 3, :each
and :all
have aliases that make their scope
more explicit: :example
is an alias of :each
and :context
is an alias of :all
. Note that :each
and :all
are not deprecated
and we have no plans to do so.
Thanks to John Feminella for implementing this.
For more info:
DSL methods yield the example as an argument
RSpec::Core::Example
provides access to all the details about an
example: its description, location, metadata, execution result, etc.
In RSpec 2.x the example was exposed via an example
method that could
be accessed from any hook or individual example:
describe MyClass do
before(:each) { puts example.metadata }
end
In RSpec 3, we’ve removed the example
method. Instead, the example
instance is yielded to all example-scoped DSL methods as an explicit
argument:
describe MyClass do
before(:example) { |ex| puts ex.metadata }
let(:example_description) { |ex| ex.description }
it 'accesses the example' do |ex|
# use ex
end
end
Thanks to David Chelimsky for coming up with the idea and implementing it!
For more info:
New expose_dsl_globally
config option to disable rspec-core monkey patching
RSpec 2.x monkey patched main
and Module
to provide top level
methods like describe
, shared_examples_for
and shared_context
:
shared_examples_for "something" do
end
module MyGem
describe SomeClass do
it_behaves_like "something"
end
end
In RSpec 3, these methods are now also available on the RSpec
module
(in addition to still being available as monkey patches):
RSpec.shared_examples_for "something" do
end
module MyGem
RSpec.describe SomeClass do
it_behaves_like "something"
end
end
You can completely remove rspec-core’s monkey patching (which
would make the first example above raise NoMethodError
) by
setting the new expose_dsl_globally
config option to false
:
# spec/spec_helper.rb
RSpec.configure do |config|
config.expose_dsl_globally = false
end
Thanks to Jon Rowe for implementing this.
For more info:
Define example group aliases with alias_example_group_to
In RSpec 2.x, we provided an API that allowed you to define example
aliases with attached metadata. For example, this is used internally to
define fit
as an alias for it
with :focus => true
metadata:
# spec/spec_helper.rb
RSpec.configure do |config|
config.alias_example_to :fit, :focus => true
end
In RSpec 3, we’ve extended this feature to example groups:
# spec/spec_helper.rb
RSpec.configure do |config|
config.alias_example_group_to :describe_model, :type => :model
end
You could use this example in a project using rspec-rails and use
describe_model User
rather than describe User, :type => :model
.
Thanks to Michi Huber for implementing this.
For more info:
New example group aliases: xdescribe
, xcontext
, fdescribe
, fcontext
Besides including an API to define example group aliases, we’ve also
included several additional built-in aliases (on top of describe
and
context
):
xdescribe
/xcontext
, likexit
for examples, can be used to temporarily skip an example group.fdescribe
/fcontext
, likefit
for examples, can be used to temporarily add:focus => true
metadata to an example group so that you can easily filter to the focused examples and groups viaconfig.filter_run :focus
.
For more info:
Changes to pending
semantics (and introduction of skip
)
Pending examples are now run to check if they are actually passing. If a pending block fails, then it will be marked pending as before. However, if it succeeds it will cause a failure. This helps ensure that pending examples are valid, and also that they are promptly dealt with when the behaviour they describe is implemented.
To support the old “never run” behaviour, the skip
method and metadata has
been added. None of the following examples will ever be run:
describe Post do
skip 'not implemented yet' do
end
it 'does something', :skip => true do
end
it 'does something', :skip => 'reason explanation' do
end
it 'does something else' do
skip
end
it 'does something else' do
skip 'reason explanation'
end
end
With this change, passing a block to pending
within an example no longer
makes sense, so that behaviour has been removed.
Thanks to Xavier Shay for implementing this.
For more info:
New API for one-liners: is_expected
RSpec has had a one-liner syntax for many years:
describe Post do
it { should allow_mass_assignment_of(:title) }
end
In this context, should
is not the monkey-patched should
that can be removed by configuring rspec-expectations to only
support the :expect
syntax. It doesn’t have the baggage that
monkey-patching Object
with should
brings, and is always
available regardless of your syntax configuration.
Some users have expressed confusion about how this should
relates
to the expect
syntax and if you can continue using it. It will continue
to be available in RSpec 3 (again, regardless of your syntax configuration),
but we’ve also added an alternate API that is a bit more consistent with
the expect
syntax:
describe Post do
it { is_expected.to allow_mass_assignment_of(:title) }
end
is_expected
is defined very simply as expect(subject)
and also
supports negative expectations via is_expected.not_to matcher
.
For more info:
Example groups can be ordered individually
RSpec 2.8 introduced random ordering to RSpec, which is very useful for surfacing unintentional ordering dependencies in your spec suite. In RSpec 3, it’s no longer an all-or-nothing feature. You can control how individual example groups are ordered by tagging them with appropriate metadata:
describe MyClass, :order => :defined do
# examples in this group will always run in defined order,
# regardless of any other ordering configuration.
end
describe MyClass, :order => :random do
# examples in this group will always run in random order,
# regardless of any other ordering configuration.
end
This is particularly useful for migrating from defined to random ordering, as it allows you to deal with ordering dependencies one-by-one as you opt-in to the feature for particular groups rather than having to solve the issues all at once.
As part of this we’ve also renamed --order default
to --order
defined
, because we realized that “default” was a highly overloaded
term.
Thanks to Andy Lindeman and Sam Phippen for helping implement this feature.
For more info:
New ordering strategy API
In RSpec 3, we’ve overhauled the ordering strategy API. What used to be
three
different
methods
is now one method: register_ordering
. Use it to define a named ordering
strategy:
# spec/spec_helper.rb
RSpec.configure do |config|
config.register_ordering(:description_length) do |list|
list.sort_by { |item| item.description.length }
end
end
describe MyClass, :order => :description_length do
# ...
end
Or, you can use it to define the global ordering:
# spec/spec_helper.rb
RSpec.configure do |config|
config.register_ordering(:global) do |list|
# sort them alphabetically
list.sort_by { |item| item.description }
end
end
The :global
ordering is used to order the top-level example groups
and to order all example groups that do not have :order
metadata.
For more info:
rspec --init
improvements
The rspec
command has provided the --init
option to setup a project
skeleton for a long time. In RSpec 3, the files it produces have been
greatly improved to provide a better out-of-the-box experience and
to provide a spec/spec_helper.rb
file with more recommended settings.
Note that recommended settings which are not slated to become future defaults are commented out in the generated file, so it’s a good idea to open the file and accept the recommendations you want.
For more info:
New --dry-run
CLI option
This option will print the formatter output of your spec suite without running any of the examples or hooks. It’s particularly useful as way to review your suite’s documentation output without waiting for your specs to run or worrying about their pass/fail status.
Thanks to Thomas Stratmann for contributing this!
For more info:
Formatter API changes
A completely new formatter API has been added that is much more flexible.
- Subscribe only to the events you care about.
- Methods receive notification objects rather than specific parameters, so new notification data can be added in a backwards compatible manner.
- Helper methods are exposed on notification objects such that inheriting
from
BaseTextFormatter
is no longer effectively necessary.
A new formatters looks like this:
class CustomFormatter
RSpec::Core::Formatters.register self, :example_started
def initialize(output)
@output = output
end
def example_started(notification)
@output << "example: " << notification.example.description
end
end
The rspec-legacy_formatters gem is provided to continue to support the old 2.x formatter API.
Thanks to Jon Rowe for taking charge of this.
For more info:
Assertion config changes
While most users use rspec-expectations, it’s trivial to use something else and RSpec 2.x made the most common alternate easily available via a config option:
# spec/spec_helper.rb
RSpec.configure do |config|
config.expect_with :stdlib
# or, to use both:
config.expect_with :stdlib, :rspec
end
However, there’s been confusion around :stdlib
. On Ruby 1.8, the
standard lib assertion module is Test::Unit::Assertions
. On 1.9+ it’s
a thin wrapper over Minitest::Assertions
(and you’re generally better
off using just that). Meanwhile, there’s also a test-unit gem
that defines Test::Unit::Assertions
(which is not a wrapper over
minitest) and a minitest gem.
For RSpec 3, we’ve removed expect_with :stdlib
and instead opted
for explicit :test_unit
and :minitest
options:
# spec/spec_helper.rb
RSpec.configure do |config|
# for test-unit:
config.expect_with :test_unit
# for minitest:
config.expect_with :minitest
end
Thanks to Aaron Kromer for implementing this.
For more info:
Define derived metadata
RSpec’s metadata system is extremely flexible, allowing you to slice and
dice your test suite in many ways. There’s a new config API that allows
you to define derived metadata. For example, to automatically tag all
example groups in spec/acceptance/js
with :js => true
:
# spec/spec_helper.rb
RSpec.configure do |config|
config.define_derived_metadata(:file_path => %r{/spec/acceptance/js/}) do |metadata|
metadata[:js] = true
end
end
For more info:
Removals
Several things that are no longer core to RSpec have either been removed entirely or extracted into an external gem:
- The Textmate formatter has been moved into the Textmate bundle. Having a formatter for one specific text editor in rspec-core doesn’t really make sense.
- RCov integration has been dropped. It was never updated to work with 1.9+ and these days we recommend using simplecov instead.
- The
--debug
CLI option has been removed. These days there are many different debugger options, and you can activate them from the command line using the--require
(or-r
) option. For example, to use byebug, pass-rbyebug
at the command line. - We’ve removed the
--line-number
CLI option. It had dubious semantics to begin with (--line-number 43
would filter to the example defined near line 43 in every loaded spec file, but there’s no reason line 43 in each file would be related), and duplicates the more tersepath/to/spec.rb:43
form. its
has been extracted into the new rspec-its gem, which Peter Alfvin has kindly offered to maintain.- Autotest integration has been extracted into the new new rspec-autotest gem (which could use a maintainer: any volunteers?).
rspec-expectations
Using should
syntax without explicitly enabling it is deprecated
In RSpec 2.11 we started the move towards eliminating monkey patching
from RSpec by introducing a new expect-based
syntax.
In RSpec 3, we’ve kept the should
syntax, and it is available by
default, but you will get a deprecation warning if you use it without
explicitly enabling it. This will pave the way for it being disabled
by default (or potentially extracted into a seperate gem) in RSpec 4,
while minimizing confusion for newcomers coming to RSpec via an old tutorial.
We consider the expect
syntax to be the “main” syntax of RSpec now,
but if you prefer the older should
-based syntax, feel free to keep using it:
we have no plans to ever kill it.
Thanks to Sam Phippen for implementing this.
For more info:
Compound Matcher Expressions
In RSpec 3, you can chain multiple matchers together using and
or or
:
# these two expectations...
expect(alphabet).to start_with("a")
expect(alphabet).to end_with("z")
# ...can be combined into one expression:
expect(alphabet).to start_with("a").and end_with("z")
# You can also use `or`:
expect(stoplight.color).to eq("red").or eq("green").or eq("yellow")
These are aliased to the &
and |
operators:
expect(alphabet).to start_with("a") & end_with("z")
expect(stoplight.color).to eq("red") | eq("green") | eq("yellow")
Thanks to Eloy Espinaco for suggesting and
implementing
this feature, and to Adam Farhi for extending
it with the &
and |
operators.
For more info:
Composable Matchers
RSpec 3 allows you to expressed detailed intent by passing matchers as arguments to other matchers:
s = "food"
expect { s = "barn" }.to change { s }.
from( a_string_matching(/foo/) ).
to( a_string_matching(/bar/) )
expect { |probe|
"food".tap(&probe)
}.to yield_with_args( a_string_starting_with("f") )
For improved readability in both the code expression and failure messages, most matchers have aliases that read properly when passed as arguments in these sorts of expressions.
For more info:
- New in RSpec 3: Composable Matchers
- rspec-expectations #280 - original discussion
- rspec-expectations #393 - implementation
- API Documentation (including list of matcher aliases)
- Relish Documentation
match
matcher can be used for data structures
Before RSpec 3, the match
matcher existed to perform string/regex
matching using the #match
method:
expect("food").to match("foo")
expect("food").to match(/foo/)
In RSpec 3, it additionally supports matching arbitrarily nested array/hash data structures. The expected value can be expressed using matchers at any level of nesting:
hash = {
:a => {
:b => ["foo", 5],
:c => { :d => 2.05 }
}
}
expect(hash).to match(
:a => {
:b => a_collection_containing_exactly(
an_instance_of(Fixnum),
a_string_starting_with("f")
),
:c => { :d => (a_value < 3) }
}
)
For more info:
New all
matcher
This matcher lets you specify that something is true of all items in a collection. Pass a matcher as an argument:
expect([1, 3, 5]).to all( be_odd )
Thanks to Adam Farhi for contributing this!
For more info:
New output
matcher
This matcher can be used to specify that a block writes to either stdout or stderr:
expect { print "foo" }.to output("foo").to_stdout
expect { print "foo" }.to output(/fo/).to_stdout
expect { warn "bar" }.to output(/bar/).to_stderr
Thanks to Matthias Günther for suggesting this (and for getting the ball rolling) and Luca Pette for taking the feature across the finish line.
For more info:
New be_between
matcher
RSpec 2 provided a be_between
matcher for objects that implement
between?
using the dynamic predicate support. In RSpec 3, we are
gaining a first class be_between
matcher that is better in a few ways:
- The failure message is much better — rather than telling you that
between?(1, 10)
returned false, it will tell youexpected 11 to be between 1 and 10
. - It works on objects that implement the comparison operators (e.g.
<
,<=
,>
,>=
) but do not implementbetween?
. - It provides both
inclusive
andexclusive
modes.
# like `Comparable#between?`, it is inclusive by default
expect(10).to be_between(5, 10)
# ...but you can make it exclusive:
expect(10).not_to be_between(5, 10).exclusive
# ...or explicitly label it inclusive:
expect(10).to be_between(5, 10).inclusive
Thanks to Erik Michaels-Ober for contributing this and Pedro Gimenez for improving it!
For more info:
Boolean matchers have been renamed
RSpec 2 had a pair of matchers (be_true
and be_false
) that mirror
Ruby’s conditional semantics: be_true
would pass for any value besides
nil
or false
, and be_false
would pass for nil
or false
.
In RSpec 3, we’ve renamed these to be_truthy
and be_falsey
(or be_falsy
, if you prefer that spelling) to make their semantics
more explicit and to reduce confusion with be true
/be false
(which read the same as be_true
/be_false
but only pass when given
exact true
/false
values).
Thanks to Sam Phippen for implementing this.
For more info:
match_array
matcher now available as contain_exactly
RSpec has long had a matcher that allows you to match the contents of
two arrays while disregarding any ordering differences. Originally,
this was available using the =~
operator with the old should
syntax:
[2, 1, 3].should =~ [1, 2, 3]
Later, when we added the expect
syntax,
we decided not to bring the operator matchers forward to the new syntax,
and called the matcher match_array
:
expect([2, 1, 3]).to match_array([1, 2, 3])
match_array
was the best name we could think of at the time
but we weren’t super happy with it: “match” is an imprecise
term and the matcher is meant to work on other kinds of collections
besides arrays. We came up with a much better name for it
in RSpec 3:
expect([2, 1, 3]).to contain_exactly(1, 2, 3)
Note that match_array
is not deprecated. The two methods behave identically,
except that contain_exactly
accepts the items splatted out individually,
whereas match_array
accepts a single array argument.
For more info:
Collection cardinality matchers extracted into rspec-collection_matchers
gem
The collection cardinality matchers — have(x).items
,
have_at_least(y).items
and have_at_most(z).items
— were one of
the more “magical” and confusing parts of RSpec. They have
been extracted into the
rspec-collection-matchers gem, which
Hugo Baraúna has graciously volunteered to maintain.
The general alternative is to set an expectation on the size of a collection:
expect(list).to have(3).items
# ...can be written as:
expect(list.size).to eq(3)
expect(list).to have_at_least(3).items
# ...can be written as:
expect(list.size).to be >= 3
expect(list).to have_at_most(3).items
# ...can be written as:
expect(list.size).to be <= 3
Improved integration with Minitest
In RSpec 2.x, rspec-expectations would automatically include
itself
in MiniTest::Unit::TestCase
or Test::Unit::TestCase
so that
you could use rspec-expectations from Minitest or Test::Unit simply
by loading it.
In RSpec 3, we’ve updated this integration in a couple ways:
- Integration with Minitest 4 (or lower) or Test::Unit is no
longer automatic. If you use rspec-expectations in such an
environment, you’ll need to
include RSpec::Matchers
yourself. - Improved integration with Minitest 5 is now provided, but you
have to explicitly load it via
require 'rspec/expectations/minitest_integration'
For more info:
Changes to the matcher protocol
As mentioned above, in RSpec 3, we no longer consider should
to be
the main syntax of rspec-expectations. We’ve updated the matcher
protocol to reflect this:
failure_message_for_should
is nowfailure_message
.failure_message_for_should_not
is nowfailure_message_when_negated
.match_for_should
(an alias ofmatch
in the custom matcher DSL) has been removed with no replacement. (Just usematch
).match_for_should_not
in the custom matcher DSL is nowmatch_when_negated
.
In addition, we’ve added supports_block_expectations?
as a new, optional part
of the matcher protocol. This is used to give users clear errors when they
wrongly use a value matcher in a block expectation expression. For
example, before this change, passing a block to expect
when using a
matcher like be_nil
could lead to false positives:
expect { foo.bar }.not_to be_nil
# ...is equivalent to:
block = lambda { foo.bar }
expect(block).not_to be_nil
# ...but the block is not nil (even though `foo.bar` might return nil),
# so the expectation will pass even though the user probably meant:
expect(foo.bar).not_to be_nil
Note that supports_block_expectations?
is an optional part of the
protocol. For matchers that are not intended to be used in block
expectation expressions, you do not need to define it.
For more info:
- rspec-expectations #270 - original discussion
- rspec-expectations #373 - implementation
- rspec-expectations #530 - original discussion of
supports_block_expectations?
- rspec-expectations #530 - implementation of
supports_block_expectations?
rspec-mocks
Using the monkey-patched syntax without explicitly enabling it is deprecated
As with rspec-expectations, we’ve been moving rspec-mocks towards a
zero-monkey patching syntax. This was originally
introduced
in 2.14. In RSpec 3, you’ll get a deprecation warning if you use the
original syntax (e.g. obj.stub
, obj.should_receive
, etc) without
explicitly enabling it (just like with rspec-expectations’ new syntax).
Thanks to Sam Phippen for implementing this.
receive_messages
and receive_message_chain
for the new syntax
The original monkey patching syntax had some features that the
new syntax, as released in 2.14, lacked. We’ve addressed that
in RSpec 3 via a couple new APIs: receive_messages
and
receive_message_chain
.
# old syntax:
object.stub(:foo => 1, :bar => 2)
# new syntax:
allow(object).to receive_messages(:foo => 1, :bar => 2)
# old syntax:
object.stub_chain(:foo, :bar, :bazz).and_return(3)
# new syntax:
allow(object).to receive_message_chain(:foo, :bar, :bazz).and_return(3)
One nice benefit of these new APIs is that they work with expect
, too,
whereas there was no message expectation equivalent of stub(hash)
or
stub_chain
in the old syntax.
Thanks to Jon Rowe and Sam Phippen for implementing this.
For more info:
- Documentation for
receive_messages
- Documentation for
receive_message_chain
- rspec-mocks #368 - discussion of
receive_messages
- rspec-mocks #399 - implementation of
receive_messages
- rspec-mocks #464 - discussion of
receive_message_chain
- rspec-mocks #467 - implementation of
receive_message_chain
Removed mock
and stub
aliases of double
Historically, rspec-mocks has provided 3 methods for creating
a test double: mock
, stub
and double
. In RSpec 3, we’ve
removed mock
and stub
in favor of just double
, and built
out more features that use the double
nomenclature (such as
verifying doubles — see below).
Of course, while RSpec 3 no longer provides mock
and stub
aliases of double
, it’s easy to define these aliases on your
own if you’d like to keep using them:
# spec/spec_helper.rb
module DoubleAliases
def mock(*args, &block)
double(*args, &block)
end
alias stub mock
end
RSpec.configure do |config|
config.include DoubleAliases
end
Thanks to Sam Phippen for implementation this.
For more info:
Verifying doubles
A new type of double has been added that ensures you only stub or mock methods
that actually exist, and that passed arguments conform to the declared method
signature. The instance_double
, class_double
, and object_double
doubles
will all raise an exception if those conditions aren’t met. If the class has
not been loaded (usually when running a unit test in isolation), then no
exceptions will be raised.
This is a subtle behaviour, but very powerful since it allows the speed of isolated unit tests with the confidence closer to that of an integration test (or a type system). There is rarely a reason not to use these new more powerful double types.
Thanks to Xavier Shay for the idea and implementation of this feature.
For more info:
Partial double verification configuration option
Verifying double behaviour can also be enabled globally on partial
doubles.
(A partial double is when you mock or stub an existing object:
expect(MyClass).to receive(:some_message)
.)
# spec/spec_helper.rb
RSpec.configure do |config|
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
end
We recommend you enable this option for all new code.
Scoping changes
rspec-mocks’s operations are designed with a per-test lifecycle in mind. This was documented in RSpec 2, but was not always explicitly enforced at runtime, and we sometimes got bug reports from users when they tried to use features of rspec-mocks outside of the per-test lifecycle.
In RSpec 3, we’ve tightened this up and this lifecycle is enforced explicitly at runtime:
- Usage of rspec-mocks features from a
before(:context)
hook (or in any other context when there is not a current example) is not supported. - Test doubles are only usable for one example. If you attempt to use a test double outside of the example in which it originated (e.g. by accidentally assigning it to a class attribute and then using it in a later example), you will get explicit errors.
We’ve also provided a new API that lets you create temporary scopes in
arbitrary places (such as a before(:context)
hook):
describe MyWebCrawler do
before(:context) do
RSpec::Mocks.with_temporary_scope do
allow(MyWebCrawler).to receive(:crawl_depth_limit).and_return(5)
@crawl_results = MyWebCrawler.perform_crawl_on("http://some-host.com/")
end # verification and resets happen when the block completes
end
# ...
end
Thanks to Sam Phippen for helping with
implementing these changes,
and Sebastian Skałacki for suggesting the new
with_temporary_scope
feature.
For more info:
any_instance
implementation blocks yield the receiver
When providing an implementation block for a method stub it can be
useful to do some calculation based on the state of the object.
Unfortunately, there wasn’t a simple way to do this when using
any_instance
in RSpec 2. In RSpec 3, the receiver is yielded
as the first argument to an any_instance
implementation block,
making this easy:
allow_any_instance_of(Employee).to receive(:salary) do |employee, currency|
usd_amount = 50_000 + (10_000 * employee.years_worked)
currency.from_usd(usd_amount)
end
employee = Employee.find(23)
salary = employee.salary(Currency.find(:CAD))
Thanks to Sam Phippen for implementing this.
For more info:
rspec-rails
File-type inference disabled by default
rspec-rails automatically adds metadata to specs based on their location on the filesystem. This is confusing to new users, and not desirable for some veteran users.
In RSpec 3, this behavior must be explicitly enabled:
# spec/spec_helper.rb
RSpec.configure do |config|
config.infer_spec_type_from_file_location!
end
Since this assumed behavior is so prevalent in tutorials, the default generated configuration still enables this.
To explicitly tag specs without using automatic inference, set the type
metadata:
RSpec.describe ThingsController, type: :controller do
# Equivalent to being in spec/controllers
end
The different available types are documented in each of the different spec types, for instance documentation for controller specs.
For more info:
Extracted activemodel mocks support
mock_model
and stub_model
have been extracted into the rspec-activemodel-mocks
gem.
Thanks to Thomas Holmes for doing the extraction and for offering to maintain the new gem.
Dropped webrat support
Webrat support has been removed. Use capybara instead.
Anonymous controller improvements
rspec-rails has long allowed you to create anonymous controllers for testing. In RSpec 3 they have received some improvements:
- By default they will inherit from the described class rather than
AppplicationController
. This behaviour can be disabled with theinfer_base_class_for_anonymous_controllers
configuration option. - Many bugfixes when using in “non-standard” contexts, such as with abstract
parents or with no
ApplicationController
. If you have had issues with anonymous controllers in the past, now would be a good time to try them again.
For more info:
- Documentation - Anonymous controllers
- rspec-rails #893 - Enable infering base class by default
- rspec-rails #905 - Fix anonymous controller route helpers
- rspec-rails #924 - Don’t assume presence of ApplicationController
Final words
As always, full changelogs are available for each for the subprojects:
RSpec 3 is the first major release of RSpec in nearly 4 years. It represents a huge amount of work from a large number of contributors.
We hope you like the new changes as much as we do, no matter how you use RSpec.
Thanks to Xavier Shay for helping write this blog post and to Jon Rowe, Sam Phippen and Aaron Kromer for proofreading it.