
shoulda-matchers gives you one-line RSpec and Minitest matchers for common Rails behavior — it { should validate_presence_of(:name) } replaces a hand-written validation spec. Add gem "shoulda-matchers", "~> 8.0" to your Gemfile’s test group, then add the configuration block to spec/rails_helper.rb naming your test framework and library. Version 8 works with Rails 8.1 and rspec-rails 8.
This article installs and configures the gem, fixes the NoMethodError every newcomer hits, and works through its matchers group by group: validations, associations and database checks, and controllers. Every example and error message was run on Ruby 3.4 with shoulda-matchers 8.0.1, rspec-rails 8.0.4, and Rails 8.1.
Getting Started
Clone the repository of this starter Rails app.
The starter-code branch has the following gems installed and set up:
You don’t need the repository to follow along — the snippets run in any Rails app with rspec-rails installed. And if you’re still deciding which specs to write in the first place, our deep dive into RSpec tests in Ruby on Rails walks through every spec type rspec-rails supports.
Shoulda Matchers for Ruby on Rails
According to the shoulda-matchers documentation:
Shoulda Matchers provides RSpec- and Minitest-compatible one-liners to test common Rails functionality that, if written by hand, would be much longer, more complex, and error-prone.
Here’s the “written by hand” version first. Our repository has an Author and Book model. We’ll add name validation to the Author model without shoulda-matchers:
RSpec.describe Author, type: :model do
describe "validations" do
it "is invalid with invalid attributes" do
expect(build(:author, name: "")).to_not be_valid
end
end
endWe build an author record without a name, and we expect it to be invalid. If we validate the name’s presence in our Author model, this spec passes. It works, but every validation you add means another hand-written example like this one — shoulda-matchers collapses each of them into a single line.
While we’ll cover shoulda-matchers with RSpec in this post, you can use other frameworks like Minitest instead.
Installation of shoulda-matchers Gem for Ruby on Rails
Add the shoulda-matchers gem to the :test group in your Gemfile. It should look like this:
group :test do
gem "shoulda-matchers", "~> 8.0"
endThen run bundle install to install the gem. Next, place this configuration at the bottom of spec/rails_helper.rb:
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
with.library :rails
end
endHere we specify the test framework and library we’ll be using.
Version 8 of the gem is tested and supported against Ruby 3.3+, Rails 7.2+, RSpec 3.x, and Minitest 5.x. On older stacks, pin an earlier major instead: 7.0.1 is the last version supporting Rails 7.1, and 6.5.0 the last supporting Rails 7.0 and earlier.
undefined method 'validate_presence_of' (NoMethodError)
This error means the matchers never loaded into your example groups: either the Shoulda::Matchers.configure block is missing from spec/rails_helper.rb, or the gem sits outside the :test group in your Gemfile, so it isn’t available in the test environment. The fix is the configuration block from the previous section, at the bottom of spec/rails_helper.rb.
Without that block, the first matcher spec you run fails like this:
Failures:
1) Author validations
Failure/Error: it { should validate_presence_of(:name) }
NoMethodError:
undefined method 'validate_presence_of' for #<RSpec::ExampleGroups::Author::Validations:0x000072fdba33ef98>The object address varies per run, and the same failure fires for every matcher — undefined method 'validate_length_of', undefined method 'belong_to', and so on. If the configuration block is in place and you still see this error, check the Gemfile: gem "shoulda-matchers" belongs in group :test (or group :development, :test), and bundle install must have run since you added it.
Active Model Spec in Rails
Your Active Model spec might consist entirely of validations similar to the hand-written spec, which shoulda-matchers handles for you. You’ll want to test validating the presence or length of certain attributes. For example, in the sample app, it’s important to validate the name presence for the author model:
describe "validations" do
it { should validate_presence_of(:name) }
it { should validate_length_of(:name).is_at_least(2) }
it { should validate_length_of(:name).is_at_most(50) }
endHere, we validate the presence and length of name — three one-liners instead of three hand-written examples. The other validation matchers follow the same shape:
| Matcher | Asserts that… |
|---|---|
validate_presence_of(:name) | the model validates the presence of name. |
validate_absence_of(:nickname) | the model validates that nickname stays blank. |
validate_length_of(:name).is_at_least(2) | the model validates the length of name; qualifiers include is_at_least, is_at_most, and is_equal_to. |
validate_numericality_of(:publication_year).is_greater_than_or_equal_to(1800) | the model validates that publication_year is a number; comparison qualifiers like is_greater_than_or_equal_to are available. |
validate_comparison_of(:pages).is_greater_than(0) | the model uses a comparison validation on pages; the qualifiers are spelled is_greater_than, is_less_than_or_equal_to, and so on. |
validate_inclusion_of(:country).in_array(["Nigeria", "Ghana"]) | the model only accepts country values from the given list. |
validate_exclusion_of(:username).in_array(["admin", "superadmin"]) | the model rejects the given username values. |
validate_confirmation_of(:password) | the model requires password_confirmation to match password. |
validate_acceptance_of(:terms_of_service) | the model requires terms_of_service to be accepted, as with a signup checkbox. |
Each matcher passes only when the matching validation exists in the model, so the spec fails the moment someone removes a validation.
Active Record Spec in Rails
In some cases, you’ll want to validate an attribute’s uniqueness, scoped to another column:
it { should validate_uniqueness_of(:title).scoped_to(:author_id) }This checks that you have a uniqueness validation for the title attribute, scoped to author_id. Unlike the other validation matchers, though, validate_uniqueness_of writes to your test database: it tests a new record against an existing one, and if no record is persisted yet, it creates one from your spec’s subject.
That trips over a Rails default. belongs_to associations are required unless you say otherwise, so on a Book model that belongs to an Author, the record the matcher tries to save has no author_id, and the one-liner fails:
Shoulda::Matchers::ActiveRecord::ValidateUniquenessOfMatcher::ExistingRecordInvalid:
validate_uniqueness_of works by matching a new record against an
existing record. If there is no existing record, it will create one
using the record you provide.
While doing this, the following error was raised:
SQLite3::ConstraintException: NOT NULL constraint failed: books.author_id
The best way to fix this is to provide the matcher with a record where
any required attributes are filled in with valid values beforehand.The fix is exactly what the message suggests — give the matcher a subject it can save:
describe "validations" do
subject { Book.new(title: "Things Fall Apart", publication_year: 1958, author: Author.create!(name: "Chinua Achebe")) }
it { should validate_uniqueness_of(:title).scoped_to(:author_id) }
endWith a valid, saveable subject, the matcher persists it as the existing record and the spec passes.
The rest of the Active Record matchers cover associations, attachments, and the database schema itself:
| Matcher | Asserts that… |
|---|---|
belong_to(:author) | the model declares belongs_to :author. |
have_many(:books) | the model declares has_many :books. |
have_one(:delivery_address) | the model declares has_one :delivery_address. |
have_and_belong_to_many(:publishers) | the model declares has_and_belongs_to_many :publishers. |
have_one_attached(:avatar) | the model declares an Active Storage has_one_attached :avatar. |
have_many_attached(:pictures) | the model declares has_many_attached :pictures. |
have_rich_text(:synopsis) | the model declares an Action Text has_rich_text :synopsis. |
accept_nested_attributes_for(:publishers).allow_destroy(true) | the model accepts nested attributes for publishers; allow_destroy and update_only qualifiers mirror the macro’s options. |
serialize(:preferences) | the attribute is serialized with the serialize macro. |
have_db_column(:title).of_type(:string) | the table has a title column, optionally of a specific type. |
have_db_index([:author_id, :title]) | the table has an index — including composite indexes — on the given columns. |
have_implicit_order_column(:updated_at) | the model sets self.implicit_order_column = "updated_at", so Book.first orders by that column instead of id. |
define_enum_for(:status).with_values([:published, :unpublished]) | the model defines an enum for status with those values. |
have_readonly_attribute(:genre) | the model marks genre with attr_readonly. |
validate_uniqueness_of(:title).scoped_to(:author_id) | the model validates the uniqueness of title within author_id — see the setup requirements described earlier in this section. |
One version footnote for the enum row: current Rails takes the enum name as a positional argument — enum :status, [:published, :unpublished] — and the old enum status: keyword form raises an ArgumentError on Rails 8.
Action Controller Spec in Rails
Controller specs are no longer the default way to test controllers — the Rails and RSpec teams both recommend request specs for new code. The controller matchers still work, though: they run in classic type: :controller specs, which rspec-rails 8 supports without extra dependencies. The separate rails-controller-testing gem is only needed if your controller specs call the extracted assigns or render_template helpers — none of the matchers in this section require it.
For a codebase that still uses controller specs, here’s the parameter matcher in action. The permit matcher checks that a controller action permits exactly the parameters you list:
RSpec.describe BooksController, type: :controller do
describe "POST #create" do
it do
params = {
book: {
title: "Tipping Point",
description: "Tipping Point",
author: 1,
publication_year: 2001
}
}
should permit(:title, :description, :author, :publication_year).
for(:create, params: params).
on(:book)
end
end
endThe params hash mirrors part of the request to the controller, and the spec passes when the create action calls params.require(:book).permit(...) with those keys. For an action that needs a query parameter — like update — include the id in the params hash and create the matching record in a before block.
The remaining controller matchers:
| Matcher | Asserts that… |
|---|---|
filter_param(:password) | the parameter is filtered from request logs via config.filter_parameters. |
permit(:title).for(:create, params: params).on(:book) | the action permits the listed parameters for the given model key. |
redirect_to(books_path) | the response redirects to the given path; issue the request in a before block first. |
respond_with(301) | the response has the given status code — a range like respond_with(301..308) also works. |
rescue_from(ActiveRecord::RecordInvalid).with(:handle_invalid) | the controller rescues the error class with the given handler method. |
use_before_action(:set_user) | the controller declares before_action :set_user; combine with should_not to assert a callback’s absence. |
use_around_action(:wrap_in_transaction) | the controller declares the given around_action. |
use_after_action(:send_admin_email) | the controller declares the given after_action. |
set_session | the action sets session data; should_not set_session fits a destroy action. |
route(:get, "/books").to(action: :index) | the route maps the verb and path to the given action, e.g. route(:get, "/books/1").to(action: :show, id: 1). |
Wrapping Up
We saw what a spec without shoulda-matchers looks like, configured the gem (and fixed the NoMethodError that appears when that configuration is missing), then worked through the validation, Active Record, and controller matchers — including the database setup validate_uniqueness_of needs on current Rails.
While it’s helpful to use shoulda-matchers, they cannot replace every spec you’ll need to write (mostly specs to do with business logic).
One-line matchers keep your model specs honest, but validations only guard the requests that reach them. AppSignal for Ruby tracks the validation errors and failing requests that slip past your test suite in production, grouped by controller action.
Happy coding!
P.S. If you’d like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!
Frequently asked questions
- How do I configure shoulda-matchers with RSpec in Rails?
- Add the shoulda-matchers gem to the test group of your Gemfile, run bundle install, then place the Shoulda::Matchers.configure block at the bottom of spec/rails_helper.rb, telling it that your test framework is RSpec and your library is Rails.
- What version of shoulda-matchers works with Rails 8?
- shoulda-matchers 8 supports Ruby 3.3 and later, Rails 7.2 and later, RSpec 3, and Minitest 5, so it works with Rails 8.1 and rspec-rails 8. For Rails 7.1, pin version 7.0.1, and for earlier Rails versions, pin 6.5.0.
- Why is validate_presence_of an undefined method in my spec?
- The Shoulda::Matchers.configure block is missing from spec/rails_helper.rb, or the gem sits outside the test group of your Gemfile, so the matchers never load into your example groups. Add the configuration block naming your test framework and library, and the NoMethodError disappears.
- Does validate_uniqueness_of save records to the database?
- Yes. The matcher tests a new record against an existing one, and if no record is persisted yet, it creates one from your spec’s subject. When a belongs_to association is required, give the matcher a subject whose associations are valid so that record can save.
Published , Updated
Wondering what you can do next?
- Subscribe to our Ruby Magic newsletter and never miss an article again.
- Start monitoring your Ruby app with AppSignal.
- Share this article on social media

Kingsley Chijioke
Our guest author Kingsley is a Software Engineer who enjoys writing technical articles.
All articles by Kingsley ChijiokeBecome our next author!
AppSignal monitors your apps
AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!


