Reusing RSpec behavior with shared examples (and when not to)
Extract repeated RSpec assertions into shared examples, but watch for hidden dependencies on let and subject.
04 Aug 2025, 04:56 UTC

The problem: the same test in three model specs
You write a spec for Order that rejects a negative total. Two weeks later you copy that block into Invoice and Subscription. When the finance team updates the rule, you edit two files and forget the third. The suite passes, but production data does not.
RSpec shared examples let you define that repeated behavior once and include it in multiple example groups. This post shows how to extract a reusable validation spec, when to prefer a plain helper method instead, and how to verify the result.
What a shared example actually does
An example group is the block created by RSpec.describe or describe. A shared example is a group of assertions stored separately and mixed into a host group with it_behaves_like.
The shared example runs inside the host group's context. That means let, subject, and before hooks defined in the host group are available inside the shared block. That implicit access is convenient, but it also creates hidden dependencies.
Worked example: a positive total across models
Assume a Ruby project with Order and Invoice models. Both must reject a negative total.
Create spec/support/shared_examples.rb:
shared_examples 'a model with a positive total' do
it 'rejects a negative total' do
subject.total = -1
expect(subject).not_to be_valid
expect(subject.errors[:total]).to include('must be greater than 0')
end
it 'accepts zero' do
subject.total = 0
expect(subject).to be_valid
end
end
Include it in each model spec:
require 'spec_helper'
RSpec.describe Order, type: :model do
subject(:order) { build(:order) }
it_behaves_like 'a model with a positive total'
end
require 'spec_helper'
RSpec.describe Invoice, type: :model do
subject(:invoice) { build(:invoice) }
it_behaves_like 'a model with a positive total'
end
Run from the project root:
bundle exec rspec spec/models/order_spec.rb spec/models/invoice_spec.rb
Expected result: both files report the two shared checks under their own group names. If Invoice lacks a total attribute, the failure appears under the Invoice group even though the shared definition is the common factor.
Shared examples versus helper methods
Use a shared example when the assertions are identical across subjects. Use a plain Ruby helper method when you need explicit parameters or clearer stack traces.
| Approach | Best for | Main drawback |
|---|---|---|
| Shared example | Identical behavior across many classes | Implicit coupling to host let and subject |
| Helper method | Explicit inputs, easy to trace | More boilerplate per spec file |
A helper version of the same check might look like this:
def expect_positive_total(record)
it 'rejects a negative total' do
record.total = -1
expect(record).not_to be_valid
end
end
You call it inside each it block. The trade-off is more code, but the dependency on record is obvious.
Limitations and how to verify
Shared examples live in a global registry. If two libraries define the same name, the last one loaded wins. Failure backtraces can point to the shared-example file rather than the spec that included it, which slows debugging.
Practical checks:
- Run
bundle exec rspec --versionin the project root. These notes assume RSpec 3.x. - Run
bundle exec rspec --format documentationand confirm the shared example name appears under each host group. - After adding a shared example to a new model, run that model's spec file and then the full suite to catch missing
subjectorletdependencies.
If a shared example breaks when reused, convert it to a helper method or pass values explicitly through shared-example arguments.
Closing
Shared examples reduce duplication, but they trade explicitness for brevity. Start with a helper method if the behavior differs even slightly. When the assertions are truly identical, extract a shared example, load it from spec_helper.rb, and verify each host group runs the expected checks.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.