ruby

How to Use Shoulda Matchers with RSpec for Ruby on Rails

Kingsley Chijioke

Kingsley Chijioke on

How to Use Shoulda Matchers with RSpec for Ruby on Rails

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.

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

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

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

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

ruby
it { should validate_absence_of(:name) }

Here's another validation spec:

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

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

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

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

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

ruby
it { should validate_uniqueness_of(:title) }

You can take it a bit further using scope:

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

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

ruby
it { should belong_to(:author) }

There are also one-line specs for other associations you might want to test:

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

ruby
it { should have_db_column(:title) }

You can take it further to test for the column type:

ruby
it { should have_db_column(:title).of_type(:string) }

There is also the option of testing for an index:

ruby
it { should have_db_index(:name) }

Even if you have a composite index:

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

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

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

ruby
it { should define_enum_for(:status) }

We can specify the test values:

ruby
it { should define_enum_for(:status).with_values([:published, :unpublished]) }

If you have a read-only attribute, you can also test for that:

ruby
it { should have_readonly_attribute(:genre) }

And you can test for accepts_nested_attributes_for:

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

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

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

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

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

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

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

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

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

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

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

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

Kingsley Chijioke

Kingsley Chijioke

Our guest author Kingsley is a Software Engineer who enjoys writing technical articles.

All articles by Kingsley Chijioke

Become our next author!

Find out more

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!

Discover AppSignal
AppSignal monitors your apps