Eliminate Test Duplication with RSpec Shared Examples: A Practical Guide
Master RSpec’s shared_examples to cut test duplication—define once, reuse everywhere, and verify with dry‑run and documentation output.
07 Jul 2025, 05:06 UTC

Why Shared Examples Matter
When a test suite grows, duplicated expectations become a maintenance nightmare. RSpec’s shared_examples lets you write a single block of expectations once and reuse it across many describe or context groups. The result is a cleaner, more consistent test surface and faster refactoring when a contract changes.
Desired Outcome
By the end of this guide you should be able to:
- Define a reusable
shared_examplesblock that encapsulates common behavior. - Include that block in multiple specs, automatically inheriting surrounding
letandsubjectdefinitions. - Parameterize the shared example so each spec can supply its own data.
- Verify that the shared example runs correctly and troubleshoot failures.
Prerequisites
- Ruby 3.x or later and Bundler installed.
- RSpec 3.10 or newer (the shared_examples API hasn’t changed in the last decade).
- A Rails or plain Ruby project with a
specdirectory configured. - Basic knowledge of
letandsubjectscopes.
Step‑by‑Step Procedure
1. Create a Shared Examples File
Put all shared examples in spec/support/shared_examples.rb to keep the spec folder tidy.
# spec/support/shared_examples.rb
RSpec.shared_examples "a valid record" do
it "is valid with valid attributes" do
expect(subject).to be_valid
end
it "is invalid when required attributes are missing" do
subject.send(:attributes).each do |attr, _|
subject.send("#{attr}=")
end
expect(subject).not_to be_valid
end
end
Note: The block uses subject and attributes so that each including spec can supply its own model instance and attribute list.
2. Load the File in Your Test Suite
RSpec automatically loads files under spec/support if you add the following to spec/rails_helper.rb or spec/spec_helper.rb:
# spec/rails_helper.rb
RSpec.configure do |config|
config.include_context "shared examples"
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
end
Alternatively, you can explicitly require the file in each spec that needs it, but loading once keeps the suite fast.
3. Use the Shared Example in a Spec
Below is a concrete example for a User model and a Post model that both share the same validation contract.
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
subject { described_class.new(attributes) }
let(:attributes) { { email: "[contact removed]", password: "secret" } }
it_behaves_like "a valid record"
end
# spec/models/post_spec.rb
require 'rails_helper'
RSpec.describe Post, type: :model do
subject { described_class.new(attributes) }
let(:attributes) { { title: "Hello", content: "World" } }
it_behaves_like "a valid record"
end
Because the shared example references subject and attributes, each spec supplies its own data while reusing the same expectation logic.
4. Run the Suite and Verify Execution
- Dry‑run to confirm discovery:
bundle exec rspec --dry-runOutput should list each example, including the shared ones, without running the tests.
- Run with documentation format:
bundle exec rspec --format documentationCheck that the shared example’s description appears under each
describeblock. - Count examples:
bundle exec rspec --dry-run | grep "examples"Each inclusion should contribute two examples (the two
itblocks).
Expected Checks
- Shared example appears in the output for every including spec.
- No undefined method errors; all
letorsubjectreferences resolve. - Test count matches expectation (e.g., 4 examples for two specs, each with two shared tests).
- No flakiness due to shared state; each example runs in isolation.
Recovery Options
- Isolate a failure: Run
bundle exec rspec spec/models/user_spec.rb:12to target the failing line. - Check context availability: If the shared example complains about an undefined
attributesmethod, ensure the including spec defines it withletorsubject. - Rename to avoid collision: If another gem defines a shared example with the same name, prefix yours (e.g.,
"myapp a valid record"). - Remove over‑parameterization: If the shared example is too generic, split it into smaller, more focused blocks.
- Verify file loading: Add
puts "Loaded shared_examples"inside the file and runrspec --dry-runto confirm it’s required before tests execute.
Benefits in a Nutshell
| Benefit | Why It Matters |
|---|---|
| Reduced duplication | One block, many uses. |
| Consistent contracts | All models enforce the same rule set. |
| Faster refactor | Change once, propagate everywhere. |
| Clear intent | Shared examples document expected behavior. |
| Isolation | Each spec supplies its own data, preventing cross‑test contamination. |
Wrap‑Up
Shared examples are a powerful RSpec feature that, when used judiciously, can dramatically reduce test clutter and enforce consistent behavior across your codebase. Keep the blocks focused on interfaces, parameterize where necessary, and always verify that the shared logic runs under each context. Happy testing!
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.