---
title: What Shoulda Matchers Is Actually Doing For You
teaser: Rails model specs full of setup and error-message checks? There’s a reason
  so many teams replace them using shoulda-matchers. We’ll look at what those expectations
  are doing, how they make your tests more resilient to changes, and a lot less noisy.
tags: rspec,rails,shoulda,ruby,testing
author: Matheus Sales
published_on: 2026-01-23
---

If you've worked on a Rails app, you've probably written or seen something like:

```ruby
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](https://thoughtbot.com/blog/the-self-contained-test)

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

```ruby
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

```ruby
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](https://thoughtbot.com/blog/building-custom-rspec-matchers-with-regular-objects) and know how to describe failures.

Conceptually:

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
it { is_expected.to have_many(:orders).dependent(:destroy) }
```

with the manual version:

```ruby
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:

```ruby
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:

```ruby
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.
