Enforcing Interface Contracts with RSpec Shared Examples
Stop duplicating tests for common interfaces. Learn how to use RSpec shared_examples to define behavioral contracts once and enforce them across multiple classes.
09 Feb 2026, 05:21 UTC

The Problem: Copy-Paste Contract Testing
When multiple classes implement the same interface—such as different storage adapters (S3, LocalDisk, Azure) or various payment gateways—you often find yourself writing the same set of tests over and over. Copying and pasting these specs is a maintenance trap: if the interface requirements change, you must update every single spec file, increasing the risk of inconsistent test coverage.
The solution is shared examples. This RSpec feature allows you to define a behavioral contract once and verify that any number of classes adhere to it, ensuring consistency across your implementation layers.
Defining the Behavioral Contract
Shared examples are defined using RSpec.shared_examples. Instead of testing a specific class, you describe the behavior expected from any object that fulfills the role. To make these examples flexible, you can pass parameters into the block.
# spec/support/shared_examples/searchable_interface.rb
RSpec.shared_examples "a searchable interface" do |required_params|
# The 'subject' is expected to be provided by the calling spec
it "raises an error if required parameters are missing" do
missing_param = required_params.first
expect { subject.search({}) }.to raise_error(ArgumentError, /missing #{missing_param}/)
end
it "returns a collection of results" do
# We assume the subject is configured to return at least one result for this test
results = subject.search(required_params)
expect(results).to respond_to(:each)
end
end
Implementing the Contract in Specs
Once defined, you can apply these examples to your specific classes. RSpec provides two primary methods for this, which differ in how they affect your test output.
Using it_behaves_like
This is the most common approach. It creates a nested context in your documentation output, which clearly separates the general interface tests from the class-specific tests.
# spec/models/user_search_spec.rb
RSpec.describe UserSearch do
subject { UserSearch.new }
it_behaves_like "a searchable interface", [:query]
it "filters by user role" do
# Class-specific test logic here
end
end
Using include_examples
If you prefer a flat output without the nested group, use include_examples. This inlines the examples directly into the current group. While it reduces nesting, it can make the output harder to parse when a large number of shared tests are included.
Comparison: Nesting vs. Inlining
| Method | Output Structure | Best Use Case |
|---|---|---|
it_behaves_like |
Nested (Grouped) | Standard interface verification where grouping adds clarity. |
include_examples |
Flat (Inlined) | Small sets of shared tests where nesting feels redundant. |
Trade-offs and Limitations
While shared examples reduce duplication, they introduce a risk known as the "Mystery Guest" problem. Because the test logic is defined in a separate file, a developer reading the spec file may not immediately understand why a test is failing without jumping between files.
Furthermore, shared examples often rely on an implicit subject. If the calling spec fails to define the subject or defines it in a way that doesn't match the shared example's expectations, you will encounter NoMethodError or NilClass errors that can be frustrating to debug.
Verification and Practical Checks
To verify your shared examples are working as intended, run your specs with the documentation formatter:
# Run from the project root with appropriate permissions
bundle exec rspec --format documentation spec/models/user_search_spec.rb
Expected Result: You should see a grouped section titled "a searchable interface" containing the specific assertions defined in your shared block. If using include_examples, those assertions will appear directly under the UserSearch description.
Closing Action
Audit your test suite for repeated it blocks across different classes. If you find three or more classes sharing the same set of assertions, extract them into a shared_examples block. This transforms your tests from a list of repetitions into a documented contract for your codebase.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.