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:
Using
should
from rspec-expectations' old:should
syntax without explicitly enabling the syntax is deprecated. Use the new:expect
syntax or explicitly enable:should
instead.
The 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 describe
and should
to every object.
This post examines how RSpec avoids monkey-patching by looking at should
and expect
specifically.
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
RSpec.world
singleton #run
is called on each of the example groups, which in turn calls#run
on all of the group's individual examples (recall that eachExample
corresponds to anit
block).
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.
That is, Example
doesn't define should
, 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-core
is responsible for executing examples within the context of their example groups. It defines the command-line executablerspec
. It also allows users to customize execution behavior by specifying example filters or output formatters.rspec-expectations
defines expectations and matchers. When expectations fail, they raise anExpectationNotMetError
, which is captured and reported by the classes inrspec-core
.
Matchers and expectations
rspec-expectations
contains two kinds of classes: matchers and expectations.
- Matchers are objects that respond to the
#matches?
and#failure_message
methods. - Expectations specify the subject under test. They do so by using methods like
expect(...).to
.
The documentation in expectations.rb
summarizes what rspec-expectations
does perfectly:
When
expect(...).to
is invoked with a matcher, it turns around and callsmatcher.matches?(<object wrapped by expect>)
.
In essence, this is all that should
and expect(...).to
really do.
How RSpec decides whether to use should
or expect
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 should
, expect
, or both. It does so via the RSpec::Expectations::Syntax
module, which responds to methods like #enable_should
and #disable_should
.
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 Object
, whereas #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 #should
and #expect
are defined on different hosts, their implementations are very similar. Both eventually call ::Expectations::PositiveExpectationHandler
or NegativeExpectationHandler
#handle_matcher
. These check whether the supplied matcher #matches?
the subject of the test. If it doesn't, the handler raises an ExpectationNotMetError
.
The main difference between the two is that the #expect
method returns an instance of ExpectationTarget
. 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.