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:

  1. How RSpec builds and runs example groups
  2. 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:

  1. Example and example groups like it and describe blocks are stored in RSpec.world.example_groups.
  2. 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:

  1. Objects returned by it "does something" are instances of RSpec::Core::Example.
  2. Objects returned by describe Thing are instances of a subclass of RSpec::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 of RSpec::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.

Note: If you found this post on RSpec useful, consider helping me write more of these posts, by supporting me on Patreon.

More on RSpec