4

How to Use Shoulda Matchers with RSpec for Ruby on Rails

 5 months ago
source link: https://blog.appsignal.com/2023/12/06/how-to-use-shoulda-matchers-with-rspec-for-ruby-on-rails.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

How to Use Shoulda Matchers with RSpec for Ruby on Rails

Kingsley Chijioke

Kingsley Chijioke on Dec 6, 2023

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.

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.

↓ Article continues below

Left squiggle

Is your app broken or slow? AppSignal lets you know.

Monitoring by AppSignal →
Right squiggle

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!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK