Coulda, woulda, shoulda-matchers
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!