How to test your Rails models with RSpec
Today, I want to share how to test Rails models with RSpec. Testing your models is a no-brainer strategy when building your applications. It gives you the confidence to make changes without regressions.
We’ll cover the fundamental testing strategies with RSpec’s built-in features. And for fun, we’ll check some extra tools to amp up your models’ coverage.
A note: I will not get into which tests are considered pertinent and which are not. This makes for more advanced practice. Keep in mind those opinions exist.
If you’re unfamiliar with RSpec, please read the first two posts of my series about RSpec fundamentals.
A Rails model in need of some tests
Let’s use a classic example: a User
model that handles data and behavior.
This User
model is composed of several elements:
- an enum
gender
that associates genders with integers in our database, - a one-to-many association with the model
Post
, - an inclusion helper,
- a public scope
by_gender
, - a public instance method
full_name
that concatenates two strings, - two private methods formatting the values provided by our user.
We now have a basic 15-line model, yet there are already a handful of things to test!
Let’s start with the simplest ones: public methods.
How to test your Rails model’s public methods
In an ideal - bug-free - world, all your public methods are tested.
Why? Because public methods are (usually) exposed through your application’s public API. Abstractions around your application can access them. Sometimes, users can too.
Since both software and people rely on your public API, you’re in a contract with them. People are consuming your application (and giving you money to do so). In return, you ensure your application behaves as expected.
Testing your models allows you to make sure you don’t break your public API when changing your codebase.
Convinced? Great! Let’s dive in.
Whether I write my tests before or after writing my code, I like to ask myself these two questions:
- What is the input?
- What is the expected output?
A lot of software engineering boils down to these two questions.
Here, full_name
takes an instance of User
as an input. It then concatenates the results of two private methods: formatted_first_name
and formatted_last_name
.
First, we’ll test our input.
Now, we have a subject full_name
(what I want to test) and a user
(the input I’ll check my behavior against).
Although my user input their first and last name with a weird case, my method should capitalize them.
Note how the formatting logic is nested in the concatenation. By testing full_name
, we indirectly test our two private methods. One stone, three birds.
Should I test my private methods?
You might have guessed the answer from the previous example, but the short answer is It Depends™.
A more comprehensive answer about whether you should test your private methods:
Your test suite should strive to test behavior not implementation. With a specific input, what output should you get?
By testing private methods, you're coupling your tests to your code implementation. This defeats tests' purpose: I can change my code as long as my public API stays the same.
However, you can see in the example above that the private methods are tested indirectly.
How to test your Rails model’s scopes
With scopes, we’re getting into a greyish area.
Testing scopes is an ongoing debate among programmers. Some feel like scopes fall under the structure umbrella - like associations - and should not be tested. Other feel scopes are behavior-driven. As I said before, I won’t take a side today, but if you ever need to test scopes, here’s how to do it.
Here, we test that the scope by_gender
returns all users with a gender set at female
. We expect our scope to return our user Buffy.
Our test passes, but a question remains: what about users excluded by our scope? We should verify these are not returned.
What happened here is we didn’t change the test, only the context. By adding a user that does not match the scope condition, we can check that:
- Our scope returns the correct users.
- The users failing our scope’s condition are not returned.
How to test your Rails model’s validations
Model-level validations are part of Rails’ strategy to ensure your database integrity. Let me show you how to test your validations with RSpec’s basic features.
Since we only have one validation, our tests are easy to write. If we had numerous validations, we’d have to add more describe
blocks testing each validation.
Testing your model’s validations, associations and enums with Shoulda Matchers
We’re now leaving RSpec built-in features to use the Shoulda Matchers library by Thoughtbot.
Shoulda Matchers provides RSpec- and Minitest-compatible one-liners to test common Rails functionality.
One of Shoulda Matchers neat tricks is that you do not need to write any context. Shoulda Matchers lean on Rails’ conventions to check your application’s functionalities.
Let me show you:
Shoulda Matchers come with qualifiers allowing you to test the options of your models’ structure. In our example, our validation checks the inclusion of the user’s gender against a list.
Shoulda Matchers cover a lot of functionalities: ActiveModel, ActiveRecord, ActionController, etc. Go and read the doc!
Here are the Shoulda Matchers tests for our model:
Easy peasy!
Is code coverage really a thing?
Now that we covered the fundamentals of testing your Rails models, I’ll give you my modest opinion on focusing on code coverage.
Code coverage is only a number and a useful number at first. Think: “We don’t have a single test in our entire codebase, and we’re afraid of making any significant changes”.
But as you move up the coverage gauge, the number means less and less.
Testing every single model’s method (known as unit testing) gives you a sense of false security. Why? Because you’re testing your methods in isolation. My experience tells me that focusing on testing whole features (integration testing) is a more productive and down-to-earth way of securing your application.
I’ll leave you with that! I hope you like this post as much as I enjoyed writing it!
Cheers,
Rémi - @remi@ruby.social
Psst, I’m working on a “Special Projects Membership Program” to up this website to eleven (More tutorials! More topics! Until the End of the Internet!). I’ll share updates about my progress over the next few months.
Sign-up to my newsletter to be the first to know! It'd mean the world to me.