Reducing Test Bloat with RSpec Shared Examples
Stop duplicating tests across similar classes. Learn how to use RSpec Shared Examples to define common behaviors once and apply them across your entire test suite.
25 Dec 2025, 06:42 UTC

The Problem: Repeating the Same Tests for Different Classes
When you build an application with multiple classes that share a common interface—such as different payment providers, various file exporters, or multiple models implementing a specific mixin—your test suite often starts to look like a copy-paste exercise. You find yourself writing the same five tests for StripePayment, then copying them for PayPalPayment, and again for BraintreePayment.
This duplication creates a maintenance burden. If the business logic for a "successful payment" changes, you must update every single spec file. The solution is Shared Examples: a way to define a set of behaviors once and apply them to any object that should exhibit those behaviors.
Defining Common Behaviors
Shared examples allow you to group tests into a reusable block. Instead of tying the tests to a specific class, you write them against a generic object (usually referred to as subject). This decouples the requirement (what the code should do) from the implementation (which class is doing it).
In RSpec, you use the shared_examples block to define the behavior and it_behaves_like to trigger those tests within a specific context. This is particularly useful for testing Interfaces (ensuring different classes respond to the same methods) and Mixins (testing a module included in multiple classes).
Worked Example: Testing a Searchable Interface
Imagine an application where both Product and Customer models must be "searchable." Both should respond to a search method that returns an array of results.
# spec/support/shared_examples/searchable_spec.rb
RSpec.shared_examples "a searchable resource" do |search_term|
it "returns results containing the search term" do
# We assume the subject is the class being tested
results = subject.search(search_term)
expect(results).to be_an(Array)
expect(results.first.to_s).to include(search_term)
end
it "returns an empty array when no matches are found" do
results = subject.search("non_existent_string_123")
expect(results).to be_empty
end
end
Now, you can apply these tests to any class that implements the search logic. Run these tests from your terminal using bundle exec rspec.
# spec/models/product_spec.rb
describe Product do
subject { Product.new }
# Pass "Laptop" as the search_term parameter to the shared example
it_behaves_like "a searchable resource", "Laptop"
end
# spec/models/customer_spec.rb
describe Customer do
subject { Customer.new }
# Pass "Jane" as the search_term parameter
it_behaves_like "a searchable resource", "Jane"
end
Verification and Diagnostics
To verify that the shared examples are executing for both classes, run RSpec with the documentation formatter:
bundle exec rspec --format documentation
You should see the shared examples nested under both the Product and Customer describe blocks in the output. If a test fails, RSpec will identify exactly which class failed the shared behavior, preventing the "generic failure" trap.
The Trade-off: Readability vs. DRY
While shared examples reduce duplication (the DRY principle), they introduce the risk of the "Mystery Guest" problem. When a developer reads it_behaves_like "a searchable resource", the actual test logic is hidden in another file. If the shared example block becomes too large or heavily parameterized, it can become difficult to understand exactly what is being tested without jumping between multiple files.
Avoid these pitfalls:
- Over-parameterization: If you find yourself passing five or six arguments into a shared example to handle edge cases for different classes, the classes are likely too different to share the same behavior. Split them into separate specs.
- Generic Subjects: Always ensure the
subjectis clearly defined in the calling spec so you know exactly what object is being passed into the shared block.
Practical Implementation Summary
Use shared examples when you have three or more classes implementing the same interface. Start by extracting the most stable requirements into a shared_examples block in spec/support. Use parameters for data-specific expectations (like search terms or expected status codes) and keep the logic focused on the behavior rather than the implementation details.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.