Michael Anhari

Coulda, woulda, shoulda-matchers

A top down view of some matcha

Good morning, good morning, good morning! Okay so the image for this post might have been a lie, I had some cold brew with a lot more caffeine than a matcha. But I couldn't resist the pun, because today we're talking about shoulda-matchas... ahem shoulda-matchers.

From the GitHub repo:

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.

Shoulda Matchers can be broken down into four categories: ActiveModel, ActiveRecord, ActionController, and independent matchers.

ActiveModel matchers

These validations work for ActiveRecord models and any classes backed by ActiveModel (i.e. form objects, which we'll cover in another post).

For example, let's say we have an Article model with a title and body.

class Article < ApplicationRecord
  validates :title, presence: true, uniqueness: true
  validates :body, presence: true
end

Here we're validating that every article has a unique title and a non-empty body. In my post on model testing in Rails we tested these validations by hand:

# test/models/article_test.rb
require 'test_helper'

class ArticleTest < ActiveSupport::TestCase
  test "the title field is required" do
    article = Article.new(title: nil, body: "I love pajamas")

    refute article.valid?
  end

  test "the body field is required" do
    article = Article.new(title: "Pajamas: How I Feel About Them", body: "")

    refute article.valid?
  end
end

Let's try re-writing them using the API provided by shoulda-matchers.

class ArticleTest < ActiveSupport::TestCase
  should validate_presence_of(:title)
  should validate_presence_of(:body)
end

Here is the full list of ActiveModel Shoulda Matchers.

ActiveRecord matchers

To test that each title is unique, we'll need to use an ActiveRecord shoulda matcher since this involves checking the record against the database.

class ArticleTest < ActiveSupport::TestCase
  should validate_presence_of(:title)
  should validate_uniqueness_of(:title)
  should validate_presence_of(:body)
end

Testing relationships between models

In our previous post covering FactoryBot, we talked about tying articles to the user who created them. In our models we would have something akin to this:

class User < ApplicationRecord
  has_many :articles
end

class Article < ApplicationRecord
  belongs_to :user

  validates :title, presence: true, uniqueness: true
  validates :body, presence: true
end

Let's test these associations directly using shoulda-matchers.

class ArticleTest < ActiveSupport::TestCase
  should validate_presence_of(:title)
  should validate_uniqueness_of(:title)
  should validate_presence_of(:body)

  should belongs_to(:user)
end

class UserTest < ActiveSupport::TestCase
  should have_many(:articles)
end

Test-driving enums

A lot of the ActiveRecord matchers involve testing the structure of our database. For example you can use have_db_column to verify that your database table actually has the columns you would expect, but I find that to be a pretty unique use case (can be useful when testing database views for example).

One I do reach for is define_enum_for. Let's take our Article example and add a status enum that tracks whether it's in_progress, published, or archived.

class Article < ApplicationRecord
  enum status: [:in_progress, :published, :archived]

  belongs_to: user

  validates :title, presence: true, uniqueness: true
  validates :body, presence: true
end

We can test that this enum is defined correctly (and more importantly, that it remains that way) by leveraging define_enum_for:

class ArticleTest < ActiveSupport::TestCase
  should validate_presence_of(:title)
  should validate_uniqueness_of(:title)
  should validate_presence_of(:body)

  should belongs_to(:user)

  should define_enum_for(:status).with_values([:in_progress, :published, :published])
end

ActionController matchers

These can be used when testing various things in the request life-cycle like setting flash messages, redirects, etc. I don't have much experience using these to be honest.

Independent matchers

There's only one in this category, delegate_method, which tests that an object forwards messages to another.

For example, let's say we wanted to frequently display the name of the user that wrote an article without having to navigate to the user object.

Let's add a quick method to display a user's full name:

class User < ApplicationRecord
  has_many :articles

  def full_name
    "#{first_name} #{last_name}"
  end
end

Great, but we're not quite there.

# the interface we currently have
article = Article.first
article.user.full_name

# the api we want
article.user_full_name

We could get there by writing a quick #full_name method in our Article model:

class Article < ApplicationRecord
  enum status: [:in_progress, :published, :archived]

  belongs_to: user

  validates :title, presence: true, uniqueness: true
  validates :body, presence: true

  def full_name
    user.full_name
  end
end

We're essentially just pushing full_name on through to the user where full_name is defined. Rails gives us an api for writing these since they are so commonly needed.

class Article < ApplicationRecord
  enum status: [:in_progress, :published, :archived]

  belongs_to: user

  validates :title, presence: true, uniqueness: true
  validates :body, presence: true

  delegate :full_name, to: :user, prefix: true
end

prefix: true tells the delegation to prepend the name of the object (user_ in this case) to the method name, which gives us the API we wanted:

article.user_full_name #=> "John Dorian"

Borat voice: Great success.

Let's update our test.

class ArticleTest < ActiveSupport::TestCase
  should validate_presence_of(:title)
  should validate_uniqueness_of(:title)
  should validate_presence_of(:body)

  should belongs_to(:user)

  should define_enum_for(:status).with_values([:in_progress, :published, :published])

  should delegate_method(:full_name).to(:user).with_prefix
end

Conclusion

That was pretty much a whirlwind tour of shoulda-matchers and the conveniences they can afford. I hope you found it helpful. Happy testing!

Newsletter

I'm working on sending out a weekly newsletter. I'll make it as easy as possible to unsubscribe at any time.