When writing tests in Rails, you should avoid repetition and have the right amount of tests to satisfy your use case.
This article will introduce you to shoulda-matchers with RSpec for testing functionality in Rails. At the end of the post, you should feel confident about using shoulda-matchers in your Rails application.
Let's get going!
Getting Started
Go ahead and clone the repository of this starter Rails app.
The starter-code
branch has the following gems installed and set up:
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.
Let's see how shoulda-matchers
will look before installing and using them. 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 end
In the above, we 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 should pass.
Note: 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', '~> 5.0' end
Then run bundle install
to install the gem. Next, place the code snippet below at the bottom of the spec/rails_helper.rb
file.
Shoulda::Matchers.configure do |config| config.integrate do |with| with.test_framework :rspec with.library :rails end end
Here we specify the test framework and library we’ll be using.
Now we'll dive into our Active Model spec.
Active Model Spec in Rails
Your Active Model spec might consist entirely of validations similar to the spec above, 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 we have above, 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)} end
Here, we validate the presence and length of name
. You can see that these validations are one-liners compared to the initial spec we created when we didn’t use shoulda-matchers.
The opposite of presence
is absence
, so we can validate that an attribute is absent like this:
it { should validate_absence_of(:name) }
Here's another validation spec:
it { should validate_numericality_of(:publication_year).is_greater_than_or_equal_to(1800) }
In the above, we test whether publication_year
is a numerical value and if it’s greater than or equal to 1800
. We can modify the comparison to look like this:
it { should validate_comparison_of(:publication_year).greater_than(1800) }
This assumes we intend to make use of validate_comparison_of
.
You can also test for validate_exclusion_of
(and its opposite, validate_inclusion_of
) like this:
it { should validate_exclusion_of(:username).in_array(['admin', 'superadmin']) } it { should validate_inclusion_of(:country).in_array(['Nigeria', 'Ghana']) }
Let's say you need to validate a password
confirmation:
it { should validate_confirmation_of(:password) }
You’ll want to validate that an attribute has been accepted where necessary. This comes in handy when dealing with terms_of_service
, for example:
it { should validate_acceptance_of(:terms_of_service) }
Next up, let's turn our attention to the Active Record spec.
Active Record Spec in Rails
In some cases, you’ll want to validate an attribute's uniqueness. This one-liner handles that:
it { should validate_uniqueness_of(:title) }
You can take it a bit further using scope:
it { should validate_uniqueness_of(:title).scoped_to(:author_id) }
This will check that you have a uniqueness validation for the title
attribute, but scoped to author_id
.
We can also test the relationship between authors and books. Let's say an author is supposed to have many books.
describe "association" do it { should have_many(:books)} end
This spec will pass if we have the relationship specified in the author model. Then, for the book model, we can have a belongs_to
spec:
it { should belong_to(:author) }
There are also one-line specs for other associations you might want to test:
it { should have_one(:delivery_address) } it { should have_one_attached(:avatar) } it { should have_many_attached(:pictures) } it { should have_and_belong_to_many(:publishers) } it { should have_rich_text(:description) }
If you want, you can test that there are specific columns in your database:
it { should have_db_column(:title) }
You can take it further to test for the column type:
it { should have_db_column(:title).of_type(:string) }
There is also the option of testing for an index:
it { should have_db_index(:name) }
Even if you have a composite index:
it { should have_db_index([:author_id, :title]) }
You can use implicit_order_column
in Rails v6+ to define the custom column for implicit ordering:
self.implicit_order_column = "updated_at"
Here, we specify that we want the updated_at
column to handle ordering. So when we run Book.first
, Rails will use the updated_at
column instead of the id
. By default, Rails uses the id
to order records.
shoulda-matchers
has a one-liner test for this:
it { should have_implicit_order_column(:updated_at) }
If we have an enum for our model (like enum status: [:published, :unpublished]
), we can write this test:
it { should define_enum_for(:status) }
We can specify the test values:
it { should define_enum_for(:status).with_values([:published, :unpublished]) }
If you have a read-only attribute, you can also test for that:
it { should have_readonly_attribute(:genre) }
And you can test for accepts_nested_attributes_for
:
it { should accept_nested_attributes_for(:publishers) } it { should accept_nested_attributes_for(:publishers).allow_destroy(true) } it { should accept_nested_attributes_for(:publishers).update_only(true) }
The above tests depend on the use case defined in your model. You can check the Rails API Documentation if you’re unsure how accept_nested_attributes_for
works.
There are also options for testing that your records are serialized when you use the serialize
macro:
it { should serialize(:books) } it { should serialize(:books).as(BooksSerializer) }
Here, we test that books
is serialized. We specify the exact serializer that we expect to use with as
.
Finally, let's turn to the Action Controller spec.
Action Controller Spec in Rails
Moving on to params, let's use config.filter_parameters
to filter parameters that we don’t want to show in our logs:
RSpec.describe ApplicationController, type: :controller do it { should filter_param(:password) } end
You can see from the above that this spec is for the ApplicationController
.
For params that will be used in other controllers when creating a record (like the BooksController
), we can have a spec that looks like this:
RSpec.describe BooksController, type: :controller 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
This will test that the right parameters are permitted for the BooksController
action. The params
hash we create matches part of the request to the controller. The test checks that title
, description
, author
, and publication_year
are permitted parameters for book
.
What if the action needs a query parameter to work?
RSpec.describe BooksController, type: :controller do before do create(:book, id: 1) end it do params = { id: 1, book: { title: 'Tipping Point', description: 'Tipping Point', author: 1, publication_year: 2001 } } should permit(:title, :description, :author, :publication_year). for(:update, params: params). on(:book) end end
In the above, we use the before
block to create a new book record with the id
as 1. Then we include the id
in the params hash.
If you have a controller action that simply redirects to another path, you can have a spec that looks like this:
describe 'GET #show' do before { get :show } it { should redirect_to(books_path) } end
This checks that we are redirected to the books_path
when the request gets to the show
action.
We can modify the above spec to also test for its response:
describe 'GET #show' do before { get :show } it { should redirect_to(books_path) } it { should respond_with(301) } end
We’ve modified it to test for the status code. If we’re not sure of the exact status code but we have a range of numbers, we can use the following:
describe 'GET #show' do before { get :show } it { should redirect_to(books_path) } it { should respond_with(301..308) } end
We can use a rescue_from
matcher to rescue from certain errors, like ActiveRecord::RecordInvalid
:
it { should rescue_from(ActiveRecord::RecordInvalid).with(:handle_invalid) }
This assumes we have (or will have) a method called handle_invalid
that will handle the error.
There are matchers for callbacks we tend to use in our controllers:
it { should use_before_action(:set_user) } it { should_not use_before_action(:set_admin) } it { should use_around_action(:wrap_in_transaction) } it { should_not use_around_action(:wrap_in_transaction) } it { should use_after_action(:send_admin_email) } it { should_not use_after_action(:send_user_email) }
You can test if the session
has been set or not.
it { should set_session } it { should_not set_session }
You’ll want to use should_not set_session
in your destroy
action.
Finally, here's how you can write a spec for your routes:
it { should route(:get, '/books').to(action: :index) } it { should route(:get, '/books/1').to(action: :show, id: 1) }
And that's it!
Wrapping Up
In this article, we’ve seen what a spec that does not use shoulda-matchers looks like. We then explored how to use shoulda-matchers for your Rails project. It simplifies specs — instead of a spec spanning multiple lines, shoulda-matchers span just one line.
While it’s helpful to use shoulda-matchers, you should know that they cannot replace every spec you’ll need to write (mostly just specs to do with business logic).
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!