Using RSpec Shared Examples Without Hiding Test Intent
Centralize reusable expectations with RSpec shared_examples, but watch for coupling and obscured intent. A practical guide with parameterized examples.
04 Aug 2025, 21:27 UTC

Duplicated validation specs are a maintenance trap
When several models need the same interface checks, teams copy the same it blocks into each spec file. A change to the business rule then requires edits in multiple places, and it is easy to miss one. The useful takeaway is that RSpec shared_examples can centralize a reusable set of expectations, but only when the shared behavior is truly the same and the coupling is explicit.
Thesis: centralize expectations, keep context visible
A shared example is a named group of examples defined once and reused. It is evaluated in the context of the example group that includes it, so subject, let and before hooks from the includer are available inside the shared block. That makes reuse possible, and it also creates an implicit dependency on the includer’s state.
Declaring and including shared examples
A shared example is declared with a description string and a block:
shared_examples 'a searchable resource' do
it 'responds to search' do
expect(subject).to respond_to(:search)
end
it 'returns an array' do
result = subject.search('query')
expect(result).to be_an(Array)
end
end
Inclusion uses it_behaves_like or include_examples. it_behaves_like nests the examples under a sub-context in the output; include_examples injects them directly into the current group.
RSpec.describe Product do
it_behaves_like 'a searchable resource'
end
RSpec.describe Category do
it_behaves_like 'a searchable resource'
end
Run the specs from the project root where the Gemfile and spec/ directory exist. The command requires read access to the spec files and execute permission for the Ruby interpreter.
bundle exec rspec spec/models/product_spec.rb
Meaningful placeholder: replace spec/models/product_spec.rb with the path to the file under test. Expected check: RSpec loads the file, expands the shared example, and reports examples for the including group. Risk: changing the subject in an includer can break the shared expectations because the shared block assumes a particular shape.
Worked example: parameterized shared examples
Static reuse is limited. Parameters let a shared example act as a template.
shared_examples 'a validatable field' do |field_name, valid_value|
it "is valid with #{field_name}=#{valid_value}" do
instance = described_class.new(field_name => valid_value)
expect(instance).to be_valid
end
end
RSpec.describe User do
it_behaves_like 'a validatable field', :email, '[contact removed]'
it_behaves_like 'a validatable field', :username, 'ruby_dev'
end
The block arguments are passed at inclusion time. The shared example uses described_class from the outer group, so each call is evaluated against the correct class. This avoids duplicating the validity check while allowing different field names and values.
Trade-offs and limitations
- Implicit coupling. A shared example relies on the includer providing
subject,letnames, or other state. Renaming aletin one spec can break several includers silently. - Obscured intent. A generic name like 'is valid' can hide what is actually being tested. Readers must open the shared definition to understand the expectations.
- Change blast radius. Editing a shared example changes every includer. That is desirable for truly shared behavior and risky for loosely related tests.
To keep intent clear, run the suite with documentation format to see how shared examples expand:
bundle exec rspec --format documentation
Run from the project root with read access to specs. Expected check: the output shows the shared example name under each including group. Risk: large suites produce long output; filter with a path placeholder to focus on relevant files.
Actionable closing
Find three or more specs with identical expectations. Extract them into a shared_examples block with a behavior-focused name. Use parameters only for values that vary, not for branching logic. If you need if statements inside a shared example to handle different classes, split the shared example. Keep each shared example focused on one behavior and document the required context, such as expected subject interface, in a comment above the definition.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.