If you’ve worked on a Rails app, you’ve probably written or seen something like:
it { is_expected.to validate_presence_of(:email) }
it { is_expected.to have_many(:orders).dependent(:destroy) }
It feels a bit like a magic trick: one short line, and a lot of behavior is being tested.
In this post, we’ll peek behind the curtain and see what these matchers are actually doing for you.
More than shorter specs
Those one-liners aren’t just shorter, they’re less fragile. Each matcher:
- Builds an isolated setup focused only on the behavior under test
- Encapsulates battle-tested patterns for exercising Rails features
- Makes your specs read like documentation and creates self-contained tests
That third point is easy to miss, so let’s make it concrete.
Imagine you wrote a presence test that happens to reuse some factory or setup. Later, you add a new validation to the model. Suddenly:
- Your old “valid” setup is now invalid, or
- An example that used to fail now passes for the wrong reason
Presence validation in practice
Say we want to test that User requires an email.
Without Shoulda Matchers
RSpec.describe User, type: :model do
it "is invalid without an email" do
user = User.new(email: nil)
expect(user).not_to be_valid
expect(user.errors[:email]).to include("can't be blank")
end
end
This works, but:
- It’s verbose
- It couples the test to specific error messages
- You’ll repeat this pattern for every attribute in every model
- If you add more validations later, this setup might fail for unrelated reasons
With Shoulda Matchers
RSpec.describe User, type: :model do
it { is_expected.to validate_presence_of(:email) }
end
Now the spec says exactly what matters:
“User validates presence of email.”
The “magic” is that the matcher hides the mechanics (building the record, calling valid?, checking errors) and focuses on the behavior you care about. As validations evolve, the matcher’s own setup logic is less likely to silently drift away from your intent.
Under the hood: it’s just Ruby
Matchers can look mysterious, but they’re just Ruby objects that respond to the matches? method and know how to describe failures.
Conceptually:
expect(user).to matcher
# becomes something like:
matcher.matches?(user)
shoulda-matchers uses that extension point to embed Rails knowledge into each matcher:
- How to trigger validations correctly
- How to introspect associations
- How to avoid common false positives
- How to construct focused setups that don’t accidentally depend on unrelated behavior
So that one line is really calling into a well-designed object that knows how to exercise that slice of Rails for you.
validate_uniqueness_of: catching subtle bugs
Uniqueness is where manually written tests tests often mislead you. A typical spec might look like:
it "is invalid when another user has the same email" do
User.create!(email: "test@example.com")
user = User.new(email: "test@example.com")
expect(user).not_to be_valid
end
This passes. But imagine you later add validates :name, presence: true. Now the test still passes, but because name is blank, not because the email is duplicated. Your uniqueness validation could be completely broken and this test wouldn’t tell you.
With shoulda-matchers:
it { is_expected.to validate_uniqueness_of(:email).case_insensitive }
That one expectation:
- Creates only strictly necessary records
- Checks case sensitivity when requested
- Focuses on the uniqueness behavior itself
- Won’t be “accidentally” satisfied by unrelated validations
- Knows about edge cases and provides helpful error messages when your test setup or validation logic has issues
Because shoulda-matchers builds its own minimal setup focused on the behavior under test, your spec is less likely to accidentally start passing or failing due to unrelated model changes. You still need higher-level tests for real-world flows, but this gives you a focused unit check with less brittleness.
have_many and friends: specs as documentation
Associations work the same way. Compare:
it { is_expected.to have_many(:orders).dependent(:destroy) }
with the manual version:
it "has many orders with dependent destroy" do
association = User.reflect_on_association(:orders)
expect(association.macro).to eq(:has_many)
expect(association.options[:dependent]).to eq(:destroy)
end
Both assert the same thing, but the matcher:
- Reads like a sentence in your domain
- Hides reflection details
- Encourages a consistent style across models
A block of matchers at the top of a spec file often gives you an instant overview of a model’s contract:
RSpec.describe User, type: :model do
it { is_expected.to validate_presence_of(:email) }
it { is_expected.to validate_uniqueness_of(:email).case_insensitive }
it { is_expected.to have_many(:orders).dependent(:destroy) }
it { is_expected.to belong_to(:account) }
end
Skimming that tells you:
- What must be present
- What must be unique
- How the record relates to others
Your tests double as living documentation and because each matcher targets one piece of behavior, they tend to age more gracefully as the model grows.
When not to use shoulda-matchers
shoulda-matchers shines for Rails-y behavior, but it’s not the right tool for everything.
You’ll usually prefer explicit specs when:
- You’re testing business logic or product rules
- You’re describing multi-step flows or user behavior
- A matcher would hide more than it reveals
A simple rule of thumb:
Will Future Me understand this expectation faster with or without a matcher?
If a plain expectation tells the story better, use that instead.
Wrapping up
When you write:
it { is_expected.to validate_presence_of(:email) }
you’re not just saving a few lines. You’re:
- Reusing a well-tested pattern for exercising Rails behavior
- Turning your specs into readable documentation
- Making tests less fragile as your models evolve
- Sharing a common testing language with your team
shoulda-matchers is a small, sharp tool that helps your tests say exactly what you mean, even as the code underneath continues to change. Once you understand the trick, it stops being magic and starts being part of your everyday toolkit.