Code Reading: Expectations in RSpec 3.0
If you've downloaded the RSpec 3.0 beta, you may have noticed that the
should method now triggers a deprecation warning:
shouldfrom rspec-expectations' old
:shouldsyntax without explicitly enabling the syntax is deprecated. Use the new
:expectsyntax or explicitly enable
expect syntax has been included since RSpec 2.11, released in July 2012:
# rather than: foo.should == bar # ...use: expect(foo).to eq(bar)
RSpec core member @myronmarston explained in 2012 that the new syntax allows RSpec to avoid monkey-patching the Ruby
Object class. Monkey-patching
Object was the source of a number of cryptic errors. In fact, one of the main goals for RSpec 3.0 has been a "zero monkey-patching mode"–a version of RSpec which does not add methods like
should to every object.
This post examines how RSpec avoids monkey-patching by looking at
rspec-core vs. rspec-expectations
In a previous post I explained what happens the command-line executable
rspec is run:
- All spec files are loaded
- As they are initialized, each example group in each spec file registers itself with the
#runis called on each of the example groups, which in turn calls
#runon all of the group's individual examples (recall that each
Examplecorresponds to an
But this isn't the whole story.
Example#run merely executes its
it block within the context of its example group. Any exceptions the block might raise are caught and reported as failures. But
Example doesn't provide a mechanism for raising these exceptions.
Example doesn't define
expect, or any other expectation methods. In fact, these methods aren't defined in
rspec-core at all–they're in a separate gem called
rspec-expectations. Each gem has a distinct set of responsibilities:
rspec-coreis responsible for executing examples within the context of their example groups. It defines the command-line executable
rspec. It also allows users to customize execution behavior by specifying example filters or output formatters.
rspec-expectationsdefines expectations and matchers. When expectations fail, they raise an
ExpectationNotMetError, which is captured and reported by the classes in
Matchers and expectations
rspec-expectations contains two kinds of classes: matchers and expectations.
- Matchers are objects that respond to the
- Expectations specify the subject under test. They do so by using methods like
The documentation in
expectations.rb summarizes what
rspec-expectations does perfectly:
expect(...).tois invoked with a matcher, it turns around and calls
matcher.matches?(<object wrapped by expect>).
In essence, this is all that
expect(...).to really do.
How RSpec decides whether to use
rspec-core autoloads the
rspec-expectations gem. Loading
rspec-expectations in turn requires many other files. One of those files contains the
RSpec::Matchers::Configuration class. This class is used to configure how
rspec-expecations behaves. For example, it provides an interface for turning off color output for expectation failures.
The configuration class also allows users to specify whether to use
expect, or both. It does so via the
RSpec::Expectations::Syntax module, which responds to methods like
A quick look at
#enable_should reveals the monkey-patching @myronmarston was referring to in his blog post–the method defines
#should as a method on
#expect is defined on the
RSpec::Matchers module. The
RSpec::Matchers module is autoloaded in
rspec-core, thus allowing developers to simply write
expect, and not
RSpec::Matchers.expect, in their spec files.
should vs. expect
Aside from the fact that
#expect are defined on different hosts, their implementations are very similar. Both eventually call
#handle_matcher. These check whether the supplied matcher
#matches? the subject of the test. If it doesn't, the handler raises an
The main difference between the two is that the
#expect method returns an instance of
ExpectationTarget#to takes a matcher argument and in turn calls
#match? on the expectation target. The
#should method does this all in a single step.
The RSpec team's drive to eliminate monkey-patching is admirable. BDD frameworks like Kiwi would benefit from similar efforts to eliminate
objc/runtime.h hacks and preprocessor macros.
Note: If you found this post on RSpec useful, consider helping me write more of these posts, by supporting me on Patreon.