Code Reading: Shared Examples in RSpec
In the first post
of this series, we examined shared examples in RSpec, what problems they
attempt to solve, and how to use them. This post is the second in a series on
shared examples in RSpec and Kiwi. All of the implementation details discussed
are accompanied by a link to the corresponding source code in
rspec-core
.
This post takes approximately 6 minutes to read, and it covers:
- How RSpec builds and runs example groups
- How RSpec registers and replays shared examples
Readers familiar with invoking RSpec to run a test suite will get the most out of this post.
The Gist
RSpec uses two singletons to store examples and shared examples:
- Example and example groups like
it
anddescribe
blocks are stored inRSpec.world.example_groups
. - Shared examples are stored in
SharedExamples.registry.shared_example_groups
.
When an #it_behaves_like
method is executed, the shared example in the
registry is retrieved and executed as a normal example group would be.
Building example groups
All sorts of setup
code
is executed when a user runs the rspec
command.
One step of this setup loads the spec files to be executed. In order to do so,
the aptly named RSpec::Core::Configuration
#load_spec_files
is
invoked.
The method does exactly what it advertises: it calls the Ruby Kernel
method
#load
on each spec
file,
causing the code within these files to be executed.
Spec files contain RSpec DSL like describe
and it
. These return one of two
objects:
- Objects
returned
by
it "does something"
are instances ofRSpec::Core::Example
. - Objects
returned
by
describe Thing
are instances of a subclass ofRSpec::Core::ExampleGroup
.
As they are created, each ExampleGroup
registers
itself
with the delightfully named singleton
RSpec.world
,
which maintains an array of example
groups
to execute. Each example group is associated with a set of examples and holds a
reference to any child example groups it may contain. For example, consider the
following spec file:
# burrito_spec.rb
describe "burrito" do
it "is delicious" do end
describe "just how delicious" do
it "is so very delicious"
end
describe "spiciness" do end
end
The "burrito" ExampleGroup
holds a reference to 1 Example
and 2 child
ExampleGroup
objects. In order to run these groups, RSpec.world
iterates
over its list of example groups and calls their #run
method.
An aside on custom formatting
ExampleGroup#run
takes an instance ofRSpec::Core::Reporter
as an argument. This class maintains an array of#listeners
which are expected to respond to methods such as#example_group_started
or#example_failed
. This is the key to RSpec's extensible output formatting system. Kiwi could borrow this technique to address issue #306, which specifically requests custom formatting.
Shared example groups
Shared example groups are stored in a registry separate from normal example
groups: the singleton object
RSpec::Core::SharedExampleGroup.registry
maintains a hash of shared
examples,
using the shared example name as a
key.
Shared examples are registered by using the RSpec::Core::TopLevelDSL
method
#shared_examples
in your spec file. This method stores the block of code associated with the
shared example in the registry.
Later, when each example group is run by RSpec.world
, any #it_behaves_like
methods encountered during execution cause a lookup to the corresponding
shared example group in the
registry.
If
found,
the block of code mapped to that key is
executed.
In this sense, #it_behaves_like
acts like a placeholder for a shared example
group.
This concludes our source code reading of the implementation of shared examples in RSpec. The next post in this series will detail a corresponding implementation in Kiwi.
If you're interested in learning more about RSpec internals, check out "Chapter 16: Extending RSpec" in The RSpec Book: Behaviour-Driven Development with RSpec, Cucumber, and Friends.