Using RSpec Shared Examples with Aggregate Failures for DRY Model Validation Tests
Learn how to extract repeated validation specs into a shared example, pass model‑specific arguments, and use aggregate_failures to see all expectation mismatches at once.
04 Jul 2025, 13:29 UTC

The problem: duplicated validation specs
When testing ActiveRecord models, it’s common to write the same presence‑validation expectations for many attributes across different describe blocks. For example, a User model might need expect(user).to validate_presence_of(:email) and the same check for :password, while a Product model repeats the pattern for :name and :price. Copy‑pasting these lines makes the suite brittle: a change in the matcher syntax requires edits in many places, and the test output becomes noisy when a failure occurs.
Thesis: shared examples + aggregate_failures give reusable, isolated tests with better failure reporting
By extracting the repeated expectations into a shared_examples block and including it with it_behaves_like, we keep the test logic in one place while preserving isolation—each inclusion gets its own scoped let bindings. Adding aggregate_failures inside the shared example lets RSpec collect all expectation mismatches before reporting, so a single test run shows every validation problem instead of stopping at the first.
Worked example: validating presence on two models
First, define a shared example that expects a model to have presence validations on a list of attributes. The example receives the model instance and the attribute names as arguments.
# spec/support/shared_examples/presence_validation.rb
RSpec.shared_examples "presence validation" do |model, attributes|
attributes.each do |attr|
it "is invalid without #{attr}" do
instance = model.new
instance.valid? # trigger validations
expect(instance.errors[attr]).to include("can't be blank")
end
end
end
Now use it in two separate describe blocks, passing the model class and the attributes to check. Note the use of it_behaves_like (preferred in RSpec 3+) and aggregate_failures to gather all errors.
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
it_behaves_like "presence validation", User, [:email, :password]
end
# spec/models/product_spec.rb
require 'rails_helper'
RSpec.describe Product, type: :model do
it_behaves_like "presence validation", Product, [:name, :price]
end
To see the benefit of aggregate_failures, wrap the expectations inside the shared example with that flag:
# spec/support/shared_examples/presence_validation.rb
RSpec.shared_examples "presence validation" do |model, attributes|
attributes.each do |attr|
it "is invalid without #{attr}" do
instance = model.new
instance.valid?
aggregate_failures "validating #{attr}" do
expect(instance.errors[attr]).to include("can't be blank")
end
end
end
end
Run the suite from the project root (you need read access to the spec files and Bundler installed):
bundle exec rspec spec/models/user_spec.rb spec/models/product_spec.rb --format documentation
Expected checks:
- If both models lack the validations, the output lists four separate failures (one per attribute) under each describe block, thanks to
aggregate_failures. - If you remove
aggregate_failures, RSpec stops after the first mismatched expectation per example, hiding the remaining errors.
Risks to watch:
- Avoid
let!or mutating instance variables inside the shared example; they can leak state between inclusions and create ordering dependencies. Preferletfor fresh values per inclusion. - Shared example descriptions inherit the host group's context, which can make failure output verbose. Use descriptive names like
:presence_validationinstead of generic labels.
Trade‑off: slight indirection for long‑term maintainability
The indirection adds a tiny cognitive step—readers must jump to the shared‑example definition to see the exact expectations. However, this cost is outweighed by the benefit of a single source of truth for validation logic, especially when the same pattern appears across dozens of models or API endpoint specs. Teams that adopt this pattern report fewer merge conflicts in spec files and faster iteration when validation rules change.
Actionable closing
Start by identifying a repeated expectation in your suite (presence, length, numericality, etc.). Extract it into a shared_examples file under spec/support/shared_examples, pass any needed values as arguments, and include it with it_behaves_like. Run rspec --format documentation to verify that the output reflects the host describe block’s description and that all failures are visible when you enable aggregate_failures. Keep an eye on mutable state and prefer let over let! to maintain test isolation.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.