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 class methods starting with
.
. Example:.method_name
- Describe instance methods starting with
#
. Example:#method_name
- Start contexts with one of these key words:
when
,with
orwithout
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 it
s.
$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.
- Declare your
let
insidedescribe
orcontext
- Do not declare your
let
insidebefore
orit
- We will talk more about
before
later
- We will talk more about
- Feel free to declare normal ruby variables inside your its
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.