How to test with RSpec: an extensive beginners guide
Tags: ruby, beginners, tutorial, testing
Published: 2023-03-03 18:10:41

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?_