DEV Community

Cover image for How to test with RSpec: an extensive beginners guide
charliekroon
charliekroon

Posted on

How to test with RSpec: an extensive beginners guide

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
topofocus profile image
Hartmut B. • Edited

Hi,
nice introduction

Personally, I do like the testing-experience of the rspec/givengem

RSpec.describe Arcade::Query do
  before( :all ) do
    ...
  end # before
  Given!( :the_document ){  TestDocument.insert name:"hugi", age: rand(99) }
  Then { the_document.is_a? Arcade::TestDocument }
  Then { the_document.name == 'hugi' }

  Given!( :creates_a_dataset ){ db.execute{ " update #{the_document.rid} set d= MAP( 'testkey', 'testvalue' ) " }.select_result }
  Then { creates_a_dataset == [1] }
  Given!( :updated_document ){ the_document.refresh }
  Then { updated_document.d == { testkey: 'testvalue' }  }
Enter fullscreen mode Exit fullscreen mode

Its much more intuitive then the traditional approach.
The expect syntax works too: Then { expect( creates_a_dataset).to be_an Array }