Notable Changes in RSpec 3

Myron Marston

May 21, 2014

Update: 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):

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.

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:

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:

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:

# 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:

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:

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-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:

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:

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:

For more info:

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.