As a Rails developer, I think most of you already have a chance working with Rspec. Sometimes you felt your code so hard to read and understandable but you don't know how to improve it. Even you already tried to reading some documents like BetterSpec or following Rspec Style Guide. Have you ever wondered if you can make the RSpec more readable?
My feeling when I try to read my code
In this post, I will share my way to improve your RSpec from my experiment including from another author I collected here. Please comment below and together we can have better guidelines for anyone need.
This post will be separated into 3 main topics:
- Structure of RSpec
- How to test your Model
- How to refactor your RSpec
But before we can go to the detail, we need to have a basic understanding of RSpec, so the easiest way is to answer the question about What is the Structure of RSpec?. You can check it here
Structure of RSpec
Overview
In this part, I will try to clear some Misunderstood and unclear things:
- Describe vs Context
- Before vs Let vs Subject
- Before & Context / Subject & Describe
Describe / Context
The block below is showing how often people using both describe and context :
describe Ranking, type: :model do
describe "ActiveModel scope" do
describe '#available' do
context 'valid target record is existed' do
#test
end
end
end
describe '#notify_user' do
context 'user allows notifying' do
#test
end
end
end
- According to the RSpec source code, context is just an alias method of describe -> No difference between these two methods
- Using both describe blocks and context blocks together
- Describe is to wrap a set of tests against one functionality -> For things
- Context is to wrap a set of tests against one functionality under the same state -> For state
describe Ranking, type: :model do
let(:user) { create(:profile, :male).user }
let(:ranking) { user.ranking }
describe "ActiveModel scope" do
describe '#available' do
before { 100.times { create(:user, last_active_at: rand(20.days).seconds.ago) } }
context 'valid target record is existed' do
before do
correct_ranking
end
it { expect(Ranking.available.count).to eq 100 }
end
context 'invalid target record has existed' do
it { expect(Ranking.available.count).to eq 0 }
end
end
end
describe "#public instance methods" do
describe '#notify_user' do
context 'user allows notifying' do
before do
ranking.update!
ranking.notify_user
end
it { expect(PushNotifications::RankingLevelJob).to have_been_enqueued }
end
context 'user account has been rejected' do
let(:user) { create(:user, status: :rejected) }
before { user.ranking.notify_user }
it { expect(PushNotifications::RankingLevelJob).to_not have_been_enqueued }
end
end
end
end
Things
Bad:
context '#calculator'
Good:
describe '#calculator'
States
Bad:
describe 'when the number is negative'
Good:
context 'when the number is negative'
Note:
- context isn't available at the top-level
- Organize your tests by things
Prefer to use context
in:
- Tests usually deal with permutations of state.
- Nested state so using nested context is the better choice
- Use describe when we have complex return values.
context
naming:
- Should not match your method names!
- Explain why we would call the method
Bad
describe "#assign"
Good
context "assigning a new value for the number A"
Before / Let / Subject
Before vs Let vs Let!
-
let block is only executed when referenced, lazy loading. This means that
let()
is not evaluated until the method that is formed is run for the first time -> only run once. -
let! blocks are executed in the order they are defined (much like a before the block). The core difference between let! and before is that you get an explicit reference to this variable, rather than needing to fall back to instance variables.
-
let! can run multiple time
-
before eagerly runs code before each example, even if the example does not use any of instance variables defined in the blocks
-
Use before for actions
-
Use let or let! for dependencies (real or test double)
Actions
Bad:
let(:dummy) do
@calculator.initialize_number
end
Better:
before do
@calculator.initialize_number
end
#or
before { @calculator.initialize_number }
Dependencies
Bad:
before { @multiple_levels = [1, 3, 5] }
Better:
let(:multiple_levels) { [1, 3, 5] }
Before vs Subject
Let vs Subject
-
let
defines a named variable -
subject
is the object being tested- is automatically based on the
describe
. - can be explicitly specified.
- can have a name.
- will be the target of "bare"
should
.
- is automatically based on the
-
Both methods are lazy loading.
-
Can use
subject!
to instantiated eagerly (before an example in its group runs) -
Can expect without writing
subject
or the name of a named subject -
Use
subject
for the thing you are testing -
Use
let
for đependencies
Bad:
let(:book) { Book.new(name: "The Hobbit", author: "J. R. R. Tolkien") }
Better:
subject(:book) { Book.new(name: "The Hobbit", author: "J. R. R. Tolkien") }
Before & Context / Subject & Describe
Before & Context
- In the official documentation don't have any mention about the relative about before & context, but when trying to understand the purpose of each method. We can see
before
aligns with thecontext
-
context
is used for the state. -
before
lists the actions to get to that state.
-
context "the number of books can be borrowed has been maximized"
before do
10.times { book_basket << Book.new }
end
end
Subject & Describe
- Like the
before & context
, the relation betweensubject & describe
is the same.subject
aligns with thedescribe
-
describe
is used for things -
subject
specifies the thing to test
-
At the top-level
describe Calculator do
subject(:cal_target) { Calculator.new(a: 5, b: 10) }
end
Can be used as nested level
describe Calculator do
subject(:cal_target) { Calculator.new(a: 5, b: 10) }
describe 'the result after calculate' do
subject(:cal_result) { cal_target.calculate }
end
end
To be continued in the next part