The Plan for RSpec 3

Myron Marston

Jul 15, 2013

Update: There’s a Japanese translation of this available now.

RSpec 2.0 was released in October 2010. In the nearly three years since then, we’ve been able to continually improve RSpec without needing to make breaking changes, but we’ve reached a point where RSpec has a fair bit of cruft stemming from the need to retain backwards compatibility with older 2.x releases.

RSpec 2.14 will be the last RSpec 2 feature release. (We may do some bug-fix patch releases, though). We’re getting started on RSpec 3, and I’d like to share our thoughts on the direction RSpec will be going.

None of this is set in stone, of course, and ultimately RSpec has been a successful project because of all of the people who use it. So please speak up if you have any thoughts about the direction we should take with RSpec 3!

What’s Being Removed

No More 1.8.6 and 1.9.1 Support

RSpec 2.x has continued to support Ruby 1.8.6 long after that version has ceased being supported by the MRI team. As an important piece of testing infrastructure in the Ruby ecosystem, we’ve felt that it’s important to allow gem authors to decide when to drop support for older Ruby versions, and not be forced to do so prematurely because the test framework they’ve chosen to use no longer supports one of the versions they support.

Ruby 1.8.6 and 1.9.1 have not been available on Travis for nearly two years, and without the safety net of a CI server running our builds on those versions, it’s become extremely difficult to continue supporting them. In practice, we’ve really only “semi-supported” these ruby versions for the last couple years: when users report issues on these ruby versions, we’ll fix them, but we haven’t been expending effort on support beyond that.

And so, the time has come to drop support for these versions. We plan to continue to support 1.8.7, 1.9.2 and all newer ruby versions on RSpec 3. Given that 1.8.7 is now legacy, we’ll probably be dropping support for 1.8.7 in RSpec 4, although if/when Travis stops supporting it before then, we’ll only be able to “semi-support” it in the same way we’ve been semi-supporting 1.8.6.

Core: its will be moved into an external gem

I’ve written about this before, so I won’t belabor the point here. We plan to move its out of rspec-core and into an external gem.

Expectations: have(x).items matchers will be moved into an external gem

RSpec originated in a time before the existence of Cucumber/Gherkin, and one of its early goals was to express things in natural language that a project stakeholder could understand. In those early days, an expression like team.should have(9).players made sense for the goals of the project. Since then, Cucumber/Gherkin have emerged as a better alternative for stakeholder-focused tests, and RSpec is rarely used for that purpose today. The have(x).items family of matchers (including the have_at_least(x).items and have_at_most(x).items siblings) are unnecessarily complicated, when a simple expression like expect(team.players.size).to eq(9) works just fine.

We plan to move these matchers out of rspec-expectations and into an external gem.

Core: No more explicit debugger support

RSpec has long supported a -d / --debug command line option for enabling the debugger via the ruby-debug gem. However, today ruby-debug is not the only (or even main) debugging gem in use today. debugger has become the de-facto standard debugging gem for MRI 1.9.2+, and many developers prefer to use pry for debugging. Other ruby interpreters like Rubinius feature their own debugger.

We plan to remove the explicit debugger support in RSpec 3. Besides removing the command line option, we’ll be removing the monkey patching of debugger in Kernel when ruby-debug is not loaded, so you will get a NoMethodError from debugger when a debugger is not loaded.

If you want to continue to load the debugger using a command line option, you can use the require flag (-r), using an option like -rdebugger.

Core: No more RCov integration

RSpec::Core::RakeTask has had some RCov options for a long time. RCov only works with MRI 1.8, and today most ruby developers use SimpleCov for their code coverage needs. SimpleCov integrates with RSpec (or any test framework) very simply, with no explicit support needed from within RSpec itself.

Core: Autotest integration will be moved into an external gem

Autotest used to be the primary ruby continuous test runner. These days, guard seems to be the more popular choice, and there’s no reason that RSpec’s Autotest integration needs to remain in rspec-core.

Core: TextMate formatter will be moved into the TextMate bundle

For many years, TextMate was the most popular text editor used by ruby developers. RSpec has had a TextMate-specific formatter for many years. These days, TextMate isn’t nearly as popular among ruby developers as it used to be, and there’s no compelling reason for the TextMate formatter to remain in rspec-core.

Lots of Deprecations

RSpec 2.14 includes many things that have been deprecated over the last couple of years. We plan to remove nearly all of the deprecated APIs and features.

What about the old expectation/mock syntax?

RSpec 2.11 introduced a new expect-based syntax for rspec-expectations. In RSpec 2.14, we updated rspec-mocks to use a similar syntax. Since introducing the new syntax, I’ve received a number of questions about how soon we will be deprecating or removing the old should-based syntax.

While I won’t say “never” (who knows what the future holds?), we don’t have any current plans to ever remove the old syntax. Users have invested in code that uses the old syntax for many years, and while we recommend using the new syntax (particularly for new projects), we’d be doing users a disservice to remove the old syntax anytime soon. It’s also not a significant maintenance burden.

For RSpec 3, we considered the idea of disabling the old syntax by default, forcing users to opt-in to use it. However, I think that doing so would be a disservice to new users who are coming to RSpec through a less-than-current tutorial. Getting a NoMethodError for an example copied from a tutorial can be very frustrating to someone trying RSpec for the first time. Experienced users can easily disable the old syntax, whereas new users aren’t likely to have enough RSpec knowledge to know to enable the old syntax used by their tutorial.

That said, we do want to encourage people to switch to the new syntax, so we plan to make RSpec 3 print a warning on first usage of any the old syntax methods (should, should_not, should_receive, etc) unless the should syntax has been explicitly enabled. This should nudge folks towards the new syntax while keeping RSpec friendly to new users and will pave the way for the old syntax to be disabled by default in RSpec 4.

What’s New

Zero Monkey Patching Mode!

Historically, RSpec has extensively used monkey patching to create its readable syntax, adding methods like describe, shared_examples_for, shared_context, should, should_not, should_receive, should_not_receive and stub to every object. In the last few 2.x releases, we’ve worked towards reducing the amount of monkey patching done by RSpec:

As discussed above, we’ll be removing RSpec’s monkey-patched Kernel#debugger in 3.0. We’re also planning to provide a config option to remove the monkey patching of the top-level DSL methods (describe, shared_examples_for, etc) onto main and Module, instead requiring you to prefix these methods with RSpec.:

RSpec.describe MyClass do
  # Within an example group you'll still be able to use
  # a bare `describe`:
  describe "#some_method" do
  end

  # And you'll be able to use a bare `shared_examples_for`:
  shared_examples_for "something" do
  end
end

RSpec.shared_examples_for "some behavior" do
end

The net result will be a set of config options (one for rspec-expectations, one for rspec-mocks and one for rspec-core), that will provide a zero-monkey-patching mode for RSpec. (We may also provide a single unified config option that sets all three).

We plan for these config options to become the defaults in RSpec 4.0, so that RSpec 4.0 will have zero monkey patching out of the box.

Mocks: Test double interface verification

It’s unfortunately been very easy to let your test doubles get out of sync with the real interfaces they are doubling. When you rename a method, or change the number of arguments a method expects, it’s easy to forget to update the test doubles you are using as standins for the changed class.

I’ve long been a fan of rspec-fire’s approach to solving this problem. I plan to port a version of it to rspec-mocks.

Take a look at the github issue where we are discussing this for the full details (the API and semantics of this feature are certainly not set in stone yet, so please voice your thoughts on that ticket!)

Expectations: Fully composable matchers

In RSpec 2.13, we added support for the include matcher to accept a list of matchers to match against. This kind of composability is quite useful and we plan to extend it to all matchers in RSpec 3. For example, you could use an expression like:

expect { |b|
  some_object.do_something(&b)
}.to yield_with_args(include(match(/foo/), match(/bar/)))

This expresses a detailed expectation: “I expect some_object.do_something to yield with a collection that includes a string matching /foo/ and a string matching /bar/”.

We’re also considering adding matcher aliases that read better when composed in this fashion, so that you could write this as:

expect { |b|
  some_object.do_something(&b)
}.to yield_with_args(a_collection_including(a_string_matching(/foo/),
                                            a_string_matching(/bar/)))

For more details, or to weigh in on this issue, take a look at the github issue.

Core: Formatter API improvements

The current API for notifying formatters of test suite progress has proved to be a bit inflexible when it comes to adding in new notifications and changing existing notifications. We’re planning to change it in a couple ways:

These changes will allow us to make further improvements that we have been unable to make in the 2.x releases. We also plan to provide a compatibility layer in RSpec 3 that wraps formatters written against the old API and adapts them to the new API so that users can more easily upgrade when they rely upon old formatters.

For more details, take a look at the github issue.

Core: DSL methods will yield the example

In RSpec 2, the current running example is exposed as example. It can be used to access the example’s metadata. This has occasionally caused problems when users inadvertently define their own example method. In RSpec 3, we’re removing the example method, opting to yield the example from each DSL method that runs in the context of an example:

describe MyClass do
  before(:each) { |example| }
  subject       { |ex| }
  let(:user)    { |ex| User.find(ex.metadata[:user_id]) }

  # before(:all) will NOT yield an example

  it "can access the current example using a block local" do |example|
    # do something with `example`
  end
end

We’re aware that this may cause upgrade headaches for users who rely on gems that use the example API (such as Capybara). We’re discussing ways to make the upgrade smoother, both for users of gem authors. For more information, see the github issue.

Expectations: Matcher protocol and custom matcher API changes

While RSpec has been moving away from its should-based syntax, the matcher protocol and custom matcher API have not changed accordingly. The matcher protocol still relies upon methods like failure_message_for_should and failure_message_for_should_not, and the custom matcher API has methods like match_for_should and match_for_should_not.

In RSpec 3, we’d like to change the matcher protocol and custom matcher API to no longer speak in terms of should while still retaining a backwards compatibility layer so that existing matchers will continue to work, with the plan to remove that compatibility layer in RSpec 4. We’re not sure what the new APIs will be yet; if you have thoughts, please chime in on the github issue.

Mocks: any_instance block implementations will yield the receiver

When stubbing a method using any_instance you can pass a block implementation just like a normal stub. However, if you wanted to access the receiver (i.e. the instance receiving the message) in the block, there was no way to accomplish this. In RSpec 3, we’re correcting this oversight, and the receiver will be yielded as the first block argument:

allow_any_instance_of(User).to receive(:age) do |user|
  ((Date.today - user.birthdate) / 365).floor
end

For backwards compatibility, we’ll be adding a config option to disable this behavior.

The Upgrade Path

Even though RSpec 3.0 will be a major release that allows us to make intentional breaking changes for the first time since 2010, it’s important to us that the upgrade path for existing test suites be as easy as possible. To that end, we’re planning a 2.99 release that will exist purely to help users upgrade. Here’s what we have in mind:

The 2.99 release will be an important step that shouldn’t be skipped in the upgrade process. It’ll give you an upgrade checklist that is specifically tailored to your test suite’s usage of RSpec, giving you a much simpler and more efficient way to upgrade than combing through changelogs trying to figure out what all is changing in RSpec 3.

The Development and Release Plan

We’ve already began working on RSpec 3 in the master branches of each of the RSpec repos. We also have a 2-14-maintenance branch for 2.14 changes (i.e. for possible patch releases) and a 2-99-maintenance branch for the changes that will be going in to 2.99. We’re planning to do multiple release candidates (and potentially some beta releases) as we make progress towards the final 3.0 release.

I won’t venture a guess for when we might release RSpec 3. Experience has taught me that software release date estimates are always wrong :(.

“How can I help?”

The current RSpec core team (David, Andy, Jon, Sam, Bradley and myself) will be driving the work for the 3.0 release…but as always, we love to get help from the community. Here are some specific ways you can help out: