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
itanddescribeblocks 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 Thingare 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#runtakes an instance ofRSpec::Core::Reporteras an argument. This class maintains an array of#listenerswhich are expected to respond to methods such as#example_group_startedor#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.