Reducing Test Duplication with RSpec Shared Examples
Learn how to use RSpec shared examples to eliminate redundant tests across similar classes, using it_behaves_like and include_examples for DRYer Ruby test suites.
30 Sept 2025, 22:04 UTC

The Problem: Redundant Assertions Across Multiple Classes
When multiple classes in a Ruby application implement the same interface or share a common behavior—such as several models implementing a Searchable module or multiple API endpoints requiring the same authentication logic—developers often copy and paste the same set of tests. This duplication increases maintenance overhead: a change in the shared behavior requires updating every single spec file, increasing the risk of inconsistent test coverage.
Prerequisites
- Ruby installed (3.0+ recommended).
- RSpec gem installed and configured in your project.
- A set of classes or modules that share a common functional contract.
Defining Shared Examples
To keep your test suite organized, define shared examples in a dedicated directory, such as spec/support/shared_examples/. Use the shared_examples block to encapsulate the behavior.
# spec/support/shared_examples/api_response_examples.rb
RSpec.shared_examples "a successful JSON response" do |expected_status|
it "returns the correct HTTP status code" do
expect(response.status).to eq(expected_status)
end
it "contains a JSON body" do
expect(response.content_type).to eq('application/json')
end
end
Implementing Shared Examples in Specs
There are two primary ways to invoke these examples: it_behaves_like and include_examples. The choice depends on how you want the test output to be structured.
Option 1: it_behaves_like (Nested Context)
This is the most common approach. It creates a nested context in your documentation output, making it clear that the object is fulfilling a specific role.
# spec/requests/users_spec.rb
RSpec.describe "Users API", type: :request do
describe "GET /profile" do
before { get "/profile", headers: { "Authorization" => "Bearer token" } }
# This creates a nested group in the output
it_behaves_like "a successful JSON response", 200
end
end
Option 2: include_examples (Flat Structure)
Use include_examples when you want the tests injected directly into the current group without adding an extra level of nesting to the output.
# spec/requests/health_spec.rb
RSpec.describe "Health Check", type: :request do
before { get "/health" }
# Tests are injected directly into this describe block
include_examples "a successful JSON response", 200
end
Handling Dependencies and State
Shared examples often rely on objects (like response or user) being available in the calling spec. To avoid “mystery guest” failures—where a test fails because a required variable is missing—explicitly define the requirements using let blocks in the calling spec.
| Approach | Pros | Cons |
|---|---|---|
| Implicit Variables | Less boilerplate in the calling spec. | Brittle; fails if the variable name changes. |
| Parameterized Arguments | Explicit and flexible. | Requires passing arguments to every call. |
Verification and Diagnostics
To verify the implementation, run your specs with the --format documentation flag. This allows you to see exactly how the shared examples are being integrated.
# Run from the project root
# Required permissions: Read access to spec files
bundle exec rspec --format documentation spec/requests/users_spec.rb
Expected Output: You should see a nested group under the specific test case: Users API GET /profile behaves like a successful JSON response returns the correct HTTP status code.
Limitations and Risks
- Traceability: Overusing shared examples can make it difficult to identify exactly which class is failing when a shared test breaks across many models.
- Complexity: Avoid nesting shared examples within other shared examples, as this creates a “deep stack” that is difficult to debug during a failure.
- Coupling: Shared examples create a tight coupling between the test definition and the calling spec's state.
Rollback
Since shared examples do not modify the application state or database, “rollback” consists of removing the it_behaves_like or include_examples call from the spec file and reverting to explicit it blocks if the shared abstraction becomes too complex to maintain.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.