Reusing Test Logic with RSpec Shared Examples
Learn how to eliminate duplicated expectations in RSpec by using shared examples with parameters, see a concrete Money/Weight example, and understand the trade‑offs.
16 Oct 2026, 03:31 UTC

Problem: Repeating the same expectations across many test files
When you have several classes that should behave identically—for example, two value objects that both implement equality, hashing, and JSON serialization—you often end up copying the same it blocks into each spec file. This duplication makes the test suite harder to maintain: a change in the contract requires editing every copy, and it’s easy to miss one.
Thesis: RSpec shared examples let you define a behavior once and reuse it safely
By extracting common expectations into a shared example and invoking it with it_behaves_like (or include_examples), you keep the contract in a single place while still getting clear, separate test output for each class.
How shared examples work
A shared example is declared with shared_examples (or shared_context) and can receive parameters via block arguments. When you include it, RSpec runs the contained expectations in the current context, as if they had been written there directly.
# spec/support/shared_examples.rb
shared_examples 'a serializable value object' do |serializer|
it 'has correct equality' do
expect(subject).to eq(subject.dup)
expect(subject).not_to eq(Object.new)
end
it 'serializes with the given serializer' do
json = serializer.call(subject)
expect(JSON.parse(json)).to be_a(Hash)
end
end
The block argument serializer lets the caller decide how to turn the object into JSON.
Worked example: testing two value objects
Suppose we have Money and Weight classes that both need equality and serialization.
# spec/money_spec.rb
require 'rspec'
require_relative '../lib/money'
require_relative '../support/shared_examples'
RSpec.describe Money do
let(:serializer) { ->(obj) { obj.to_json } }
it_behaves_like 'a serializable value object', serializer
end
# spec/weight_spec.rb
require 'rspec'
require_relative '../lib/weight'
require_relative '../support/shared_examples'
RSpec.describe Weight do
let(:serializer) { ->(obj) { obj.to_json } }
it_behaves_like 'a serializable value object', serializer
end
Running the suite executes the same four expectations for each class:
$ rspec spec
Money
has correct equality
serializes with the given serializer
Weight
has correct equality
serializes with the given serializer
Finished in 0.02 seconds (files took 0.12 seconds to load)
4 examples, 0 failures
Trade‑off: clarity vs. traceability
Shared examples reduce duplication, but because the expectations live outside the spec file, a failure message shows only the line where it_behaves_like appears. If the shared example grows large, it can become hard to see which expectation caused the failure without opening the shared‑example file.
To keep the benefit while limiting this drawback:
- Focus each shared example on a single responsibility (e.g., equality, serialization, interface contract).
- Pass only the values that vary; avoid reaching into the enclosing scope.
- If a shared example becomes too complex, split it into smaller, named pieces.
Actionable steps to try it yourself
- Create a minimal Ruby project (or use an existing one) and add RSpec:
gem install rspec. - Place the shared example in
spec/support/shared_examples.rb(ensure this directory is required inspec/spec_helper.rb). - Write two spec files that each call
it_behaves_likewith the same shared example but different parameters. - Run
rspecfrom the project root. You should see the expectations executed for each class. - Modify the shared example (add an expectation or change a parameter) and run
rspecagain; both specs should reflect the change. - Observe the output: if a test fails, the failure trace will point to the line with
it_behaves_like; open the shared‑example file to see the exact expectation that failed.
Risk: Changing a shared example affects every spec that includes it. Treat shared examples as part of your test contract and version them carefully—consider adding a comment or a deprecation notice when you need to break compatibility.
Closing
RSpec shared examples are a straightforward way to keep your test suite DRY without sacrificing the clarity of individual test runs. By defining reusable behavior, passing only the needed parameters, and keeping each example focused, you gain consistency and reduce maintenance overhead while still getting precise feedback when something goes wrong.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.