Reusing Test Logic in RSpec with Shared Examples and Shared Contexts
Learn how to eliminate duplicated test logic in RSpec using shared examples and shared contexts, with a concrete worked example and practical tips for maintenance.
25 Oct 2025, 20:29 UTC

The Problem: Duplicated Expectations Across Specs
When testing similar behavior in different parts of an application—say, verifying that both a User model and a Presenter object respond to #name and return a non‑empty string—you often end up copying the same expect blocks into multiple spec files. This duplication makes the suite harder to maintain: a change in the expected format requires editing every copy, and it’s easy to miss one.
Solution Overview: Shared Examples and Shared Contexts
RSpec provides two mechanisms to extract reusable test logic:
- Shared examples (
shared_examples) define a set of expectations that can be included in any example group withit_behaves_likeorinclude_examples. - Shared contexts (
shared_context) define reusable setup—before,after,let, helper methods—that can be mixed in withinclude_context.
Both support metadata filtering, so you can apply the shared behavior only to specs that match certain tags (e.g., :type => :model).
Worked Example: Testing a Readable Object
Suppose we have two classes that should behave like a readable object: they expose a #read method returning a string and raise IOError when the underlying source is unavailable.
# spec/shared/readable_object_shared_examples.rb
shared_examples 'a readable object' do
let(:source) { described_class.new } # the class under test is available via described_class
it 'returns a string from #read' do
expect(source.read).to be_a(String)
end
it 'does not return an empty string' do
expect(source.read).not_to be_empty
end
context 'when the source is unavailable' do
before { allow(source).to receive(:available?).and_return(false) }
it 'raises IOError' do
expect { source.read }.to raise_error(IOError)
end
end
end
Now we can reuse this in two different spec files:
# spec/models/document_spec.rb
require 'rails_helper'
RSpec.describe Document, type: :model do
it_behaves_like 'a readable object'
# Document‑specific specs go here
end
# spec/presenters/pdf_presenter_spec.rb
require 'rails_helper'
RSpec.describe PdfPresenter do
it_behaves_like 'a readable object'
# Presenter‑specific specs go here
end
To run the specs, execute from the project root:
bundle exec rspec spec/models/document_spec.rb spec/presenters/pdf_presenter_spec.rb
You should see output similar to:
Document
behaves like a readable object
returns a string from #read
does not return an empty string
when the source is unavailable
raises IOError
PdfPresenter
behaves like a readable object
returns a string from #read
does not return an empty string
when the source is unavailable
raises IOError
If you later decide that a readable object should also respond to #size, you only edit the shared example:
# spec/shared/readable_object_shared_examples.rb
shared_examples 'a readable object' do
# …existing expectations…
it 'responds to #size with the length of the read string' do
expect(source.size).to eq(source.read.length)
end
end
Re‑run the same command; both Document and PdfPresenter specs will now include the new expectation without any further changes.
Trade‑offs and When to Avoid Overuse
While shared constructs reduce boilerplate, they can obscure the flow of an individual spec if overused:
- Deep nesting of
include_contextmakes it difficult to see what state is actually set up for a given example. - If a shared example relies on instance variables defined in the consuming group, a change in one consumer can silently break others. Prefer explicit
letbindings or pass data via a block toit_behaves_like. - Metadata filtering adds another layer of indirection; ensure tags are documented so future maintainers know why a spec is (or isn’t) including the shared behavior.
A practical way to check that a shared example is being applied as expected is to add a temporary expectation that prints a unique message, run the suite, and verify the message appears under each consuming group’s description in the output.
Actionable Checklist
- Identify duplicated expectation blocks across spec files.
- Extract them into a
shared_examplesblock in a dedicatedspec/shared/directory. - Replace the duplicated blocks with
it_behaves_like 'your shared name'. - If you need reusable setup (e.g., common
letdefinitions), create ashared_contextand include it withinclude_context. - Run the affected specs with
bundle exec rspecto confirm they still pass. - When modifying shared logic, run the entire suite (or at least all specs that include the shared piece) to verify no regressions.
- Document any metadata filters used, and avoid nesting more than two levels of shared contexts for clarity.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.