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
.
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:
Then run bundle install
to install the gem. Next, place the code snippet below at the bottom of the spec/rails_helper.rb
file.
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.
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:
Here's another validation spec:
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:
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:
Let's say you need to validate a password
confirmation:
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:
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:
You can take it a bit further using scope:
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.
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:
There are also one-line specs for other associations you might want to test:
If you want, you can test that there are specific columns in your database:
You can take it further to test for the column type:
There is also the option of testing for an index:
Even if you have a composite index:
You can use implicit_order_column
in Rails v6+ to define the custom column for implicit ordering:
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:
If we have an enum for our model (like enum status: [:published, :unpublished]
), we can write this test:
We can specify the test values:
If you have a read-only attribute, you can also test for that:
And you can test for accepts_nested_attributes_for
:
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:
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:
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:
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?
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:
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:
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:
We can use a rescue_from
matcher to rescue from certain errors, like ActiveRecord::RecordInvalid
:
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:
You can test if the session
has been set or not.
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:
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!