I had only been coding for a year when I started working at HackerOne. At HackerOne, we primarily use Ruby on Rails on the backend. However, I only knew JavaScript and TypeScript, so I had to learn Ruby on Rails. On top of that, I had to learn how to write tests with RSpec. Nothing confused me more than the official RSpec documentation, and there was little online content. Because I don’t want you to struggle as I have, I’ve written a beginner’s guide to help you get started with RSpec.
What is RSpec
RSpec is a testing framework used to test Ruby on Rails code. It is designed for behavior-driven development (BDD), which originates from Test Driven Development (TDD). In simpler terms, RSpec allows you to write tests that describe how you want your code to behave. RSpec is intended to test the backend of your app only, as it is solely Rails-focused.
What to test for
Before writing any tests, it’s important to know what to test for. A good rule of thumb is to always look at the implementation first, as it provides guidance when writing a test. As a general rule, you should always test for the following:
- What happens in the happy path?
- What happens when I create an error or add invalid input? (check the validation)
- Am I the right person to make this action? (check the authorization)
Now, imagine we would be creating a second Instagram app. One of the most important models for that app would be the user model. Before we get into testing with Rspec, let’s write a very simple user model.
# models/user.rb
class User < ApplicationRecord
validates :name, presence: true
validates :email, presence: true, uniqueness: true
has_secure_password
end
Great! Now that we’ve got our user model we are ready to write our test. Remember what we just spoke about? To know what we want to test for we want to look at our implementation first. If you had to explain in human language what our model does, you could say that:
- it is valid when it has a name, email, and password
- it is invalid without a name
- it is invalid without an email
- it is invalid without a password
Without realizing it, you already wrote half of our test. Let’s finish writing the test that covers the requirements we just described:
# spec/models/user_spec.rb
require 'rails_helper
RSpec.describe User, type: :model do
it 'is valid with a name, email, and password' do
user = User.new(
name: 'Tanya McQuoid',
email: 'tanya@thewhitelotus.com',
password: 'yougotthis'
)
expect(user).to be_valid
end
it 'is invalid without a name' do
user = User.new(name: nil)
user.valid?
expect(user.errors[:name]).to include("can't be blank")
end
it 'is invalid without an email' do
user = User.new(email: nil)
user.valid?
expect(user.errors[:email]).to include("can't be blank")
end
it 'is invalid without a password' do
user = User.new(password: nil)
user.valid?
expect(user.errors[:password]).to include("can't be blank")
end
it 'is invalid with a duplicate email address' do
User.create!(
name: 'Tanya McQuoid',
email: 'tanya@thewhitelotus.com',
password: 'yougotthis'
)
user = User.new(
name: 'Shane',
email: 'tanya@thewhitelotus.com',
password: 'pineappleroom'
)
user.valid?
expect(user.errors[:email]).to include('has already been taken')
end
end
Let’s break down step by step what we just wrote.
'require 'rails_helper'
RSpec needs to know where your Rails app is and where to find its components. With 'require 'rails_helper'
we are loading the Rails application environment, and require access to the rails app.
describe
describe
outlines the general functionality of the class or feature you're testing. The describe
method has a set of expectations, for example, what a model should look like.
it
With every line that starts with it
a new, individual test case begins. it
takes a string argument that describes what the test is testing.
If you want to temporarily exclude a test example for your test without deleting it, you can use xit
instead of it
.
User.create
vs. User.new
In Ruby on Rails User.new
and User.create
are both methods that can be used to create a new instance of the User class. Though they are similar, they do work slightly differently. User.new
creates a completely new instance of the User model, but does not save it to the database. To save it to the database, we need to run a separate user.save command. With User.create!
you create a new user record and immediately save it to the database in one go.
Within the block it "is invalid with a duplicate email address"
we are testing if the email already exists. First, we create a new user record and save it directly to the database with the User.create!
command. The !
at the end of the method will help us by raising an error if the user can’t be saved.
Next, we use User.new
to create a new instance of the User model with a different name and password, but with the same email. We don’t use User.create!
here, because we don’t want to actually save the record. We are only interested in testing its validation.
expect
The expect
method allows us to make assertions about the expected code behavior. For example, by writing, expect(user).to be_valid
we are making the assertion that the user object is valid.
Additional things
When starting out with RSpec, there were things that confused me, and that I couldn’t find a lot of information about online. We didn’t use them in our code example earlier, but I think they are important to discuss. Let’s take a look at them together.
describe
vs. context
In RSpec, context
is a synonym for describe. The only difference between the two is that context
is meant to indicate that the tests inside it are related to a specific context, while describe
is more general. Some people prefer to use context
when they're describing a specific scenario, while others use describe
for everything. It's mostly a matter of personal preference.
subject
subject
lets you declare a single test subject, that is reused throughout our tests. For example, we might write:
RSpec.describe User do
subject { User.new(
name: 'Tanya McQuoid',
email: 'tanya@thewhitelotus.com',
password: 'yougotthis') }
it 'is valid with valid attributes' do
expect(subject).to be_valid
end
it 'is invalid without a name' do
subject.name = nil
expect(subject).to_not be_valid
end
end
In this example, we are using subject
to create a new valid User object that will be used in our tests. We can then reference subject
in our test assertions without having to create a new User object each time.
before
and after
before
and after
are hooks that allow you to run your code before and after each example in your test. The before
hook is typically used to set up data or variables that are required for your test examples to run.
RSpec.describe User do
before do
@user = User.create(
name: "Armond",
email: "armond_themanager@thewhitelotus.com",
password: "password")
end
it "validates the presence of a name" do
@user.name = nil
expect(@user).to_not be_valid
end
it "validates the presence of an email" do
@user.email = nil
expect(@user).to_not be_valid
end
end
The after
hook is used to clean up (e.g. deleting database records or closing network connections) the data that was created during our test.
let
and let!
let
is an RSpec method that is lazily defined. Meaning that this method won't be assigned before it's called somewhere in the test. let
memoizes the content within its block, which means it’s not going to change the context and it's only going to call and evaluate the context once.
let!
is not lazily defined and thus once you define it, the method will be created immediately, even though it's not being called.
We are using let
by default, but when you’re testing multiple things, and you need to update that test variable multiple times with different expectations, you can use let!
. Then again, a counterargument is that the thing you’re testing is deserving of its own test case.
RSpec is a super powerful tool, but it can be overwhelming at first. It will take some time to learn how to use the framework but by practicing you can learn how to use it effectively. But once you get used to it, writing tests with Rspec will feel like second nature.
What are your experiences with RSpec? How did you learn to use it effectively? And what are your approaches to writing great specs with it?
Top comments (1)
Hi,
nice introduction
Personally, I do like the testing-experience of the
rspec/given
gemIts much more intuitive then the traditional approach.
The expect syntax works too:
Then { expect( creates_a_dataset).to be_an Array }