Caroline Salib

First day using RSpec

Before starting I want to share two resources I like to use when writing tests, Better Specs and RSpec - Relish. I learned how to write tests by checking these two sites and therefore many of the examples I'll give will be based on that.

Basic structure

The first thing to notice when writing tests with RSpec is the structure of it, expect, subject, describe and context.

The methods it and expect are necessary to write an effective test.

describe DumbClass do
  it "returns true" do
    expect(1 == 1).to eq(true)
  end
end

The subject is exactly what the name says, it is the subject of our test. Usually the subject is an instance of the class we are testing.

describe DumbClass do
  subject { DumbClass.new }
  # or
  subject { described_class.new }
end

Note that we can use the method subject without declaring it. RSpec will automatically call described_class.new in this case.

describe DumbClass do
  it "returns the subject class" do
    expect(subject.class).to eq(DumbClass) # it will pass
  end
end

Even though describe and context do the same thing under the hood, they are meant to be used in different situations. We should use describe to describe our classes and methods and context to organize our tests scenarios.

Best practices tips:

describe CarRental do
  describe "#rent" do
    context "when there is a car available" do
      it "returns rented car" do
        expect(subject.rent.class).to eq(Car)
      end
    end

    context "when there is no car available" do
      it "returns false" do
        expect(subject.rent).to eq(false)
      end
    end
  end
end

Sometimes we will see RSpec.describe in the beginning of the test instead of describe. Both work the same. I prefer using just describe because it's cleaner.

"Let" me tell you something

We use let to declare "variables" for our tests. The reason to use let instead of regular Ruby variables is that let will cache the result of each call in the same it, but not in different its.

$count = 0
describe "let" do
  let(:count) { $count += 1 }

  it "does not change count value in the same example" do
    expect(count).to eq(1)
    expect(count).to eq(1)
  end

  it "is not cached across examples" do
    expect(count).to eq(2)
  end
end

# Run this using --order defined to respect the order of the expectations
# bundle exec rspec spec/dumb_spec.rb --order defined

The difference between using let and let! is that let is lazy-evaluated the first time it is called and let! is evaluated before each it. The most common use case to use let! is when we are testing an object and we need something to be created before the test run, like a database record, for example.

describe FindUser do
  let!(:user) { User.create(name: "User Test") }

  describe ".by_name" do
    it "finds user by name" do
      # User with name "User Test" needed to be created first so it will return in this method
      expect(FindUser.by_name("User Test")).to eq(user)
    end
  end
end

One thing that is confusing about let is that in order to work it can only be declared in certain parts of our tests.

describe CarRental do
  # Inside a describe: works
  let(:example) { User.create(name: "User Test") }

  describe "#rent" do
    context "when there is a car available" do
      # Inside a context: works
      let(:example) { User.create(name: "User Test") }

      before do
        # Inside an before: doesn't work
        let(:example) { User.create(name: "User Test") }
      end

      it "returns rented car" do
        # Inside an it: doesn't work
        let(:example) { User.create(name: "User Test") }

        new_variable = "User Test" # this is ok
        expect(subject.rent.class).to eq(Car)
      end
    end
  end
end

Before and After hooks

The hooks before and after are available in RSpec so we can setup the environment for the test. Anything we need to happen before the test, like persisting some data to test a query, or after the test, like deleting data that can affect other tests.

In the example below, we want our method to "do something" when the redis key is true, and "do nothing" when the redis key is not present. We need to set that redis key to true before the happy path scenario, and delete it so it will continue to be non-existent in other scenarios.

describe DumbTest do
  describe '#call' do
    it 'does nothing' do
      # ...
    end

    context "when redis key is true" do
      before do # before(:each)
        $redis.set("MY_REDIS_KEY_#{show_id}", true)
      end

      after do # after(:each)
        $redis.del("MY_REDIS_KEY_#{show_id}")
      end

      it 'does something' do
        # ...
      end
    end
  end
end

Keep in mind that when we don't specify the argument when calling these hooks, it will use :each by default. There are different types of ways we can use before and after by change the param, for example, before(:all) and after(:all). Read more about it here.

FactoryBot - creating fake data

First we need to keep in mind that when we are testing models in Rails, all the data is persisted in a test database and not in our regular development database. Now that we have this clear, let's talk about how to persist data.

Usually when working with Rails and RSpec, we will use a gem called factory_bot to help us persist data. When using FactoryBot we are able to generate ActiveRecord models templates with pre populated data, including the model relations which can be very handy.

# spec/factories/user.rb
FactoryBot.define do
  factory :user do # It will match a Active Record model called User 
    name { 'Josh' }
    email { 'josh@test.com' }
  end
end

To use the created factory:

# spec/models/user_spec.rb
describe User do
  it "validates user attributes" do
    user = create(:user) # FactoryBot.create(:user)

    expect(user.name).to eq("Josh")
  end
end

On the "dumb test" above, we created an user factory and check that it was using the pre-populated name we gave it. Because we used the method create, the user was inserted in the database, but if we think for a bit, we don't need the user to be persisted in the database to test that. Good news, Factory Bot has a method called build that will create the factory but not persist in the database. It's always preferable to use build unless we really need the data to be persisted because we can make our tests faster by avoiding database transactions.

# spec/models/user_spec.rb
describe User do
  it "validates user attributes" do
    user = build(:user)
    expect(user.name).to eq("Josh")
  end
end

I suggest taking a look at devhints.io/factory_bot to learn more about what we can do with FactoryBot. Take a look at create_list, association and trait. Those can be very handy.

In addition to FactoryBot, another good gem is faker. Used to generate fake data. It has all sorts of generators such as names, numbers, texts, images, codes, addresses and a bunch more. Check the list of things Faker can generate: https://github.com/faker-ruby/faker#default

An example of its usage in our factories and tests:

# spec/factories/user.rb
FactoryBot.define do
  factory :user do # It will match a Active Record model called User 
    name { Faker::Name.name }
    email { Faker::Internet.email }
  end
end

# spec/models/user_spec.rb
describe User do
  it "checks that user is valid" do
    user = build(:user, password: Faker::String.random(length: 6))
    expect(user).to be_valid
  end
end

Yey!! Thanks for reading and good luck on your first day with RSpec. Feel free to contact me if you have any questions or feedback regarding this post.