loading...

RSpec let! and before order dependent behavior

prathamesh profile image Prathamesh Sonpatki ・2 min read

RSpec provides let andlet! helpers to create data required by the specs.

We can use them as follows:

RSpec.describe Activity, type: :model do
  let!(:user) { FactoryBot.create(:user) }
  let(:account)  { FactoryBot.create(:account) }
end

let is a memoized method which gets called only when it is referenced in the specs. As it is memoized, if it is called twice, it is not executed again. let! is called before every spec. It is not memoized like let.

So in our example above, user is created before every spec, whereas account is created only once and is memoized.

Rspec also has a before hook which we can be used to execute some code before all the specs are run in a specific block.

RSpec.describe Activity, type: :model do
  let!(:user) { FactoryBot.create(:user) }
  let(:account) { FactoryBot.create(:account) }
  before do
    travel_to Time.new(2019, 10, 27, 10, 00, 00)
  end
end

We want to set the current time to a specific time for all specs so we added it in the before block.

In my spec, I was setting such custom time in the before block and I was expecting user's created_at time to be set to that custom time. The user was created using let! just like in the example above. But the user was always getting set with current time as created_at instead of custom time set in the before block.

To figure out why this was happening, I decided to see how let! is implemented. Turns out let! is implemented as before hook under the hood. So I have two before hooks.

  before do
    user = FactoryBot.create(:user)                                              
  end
  let(:account) { FactoryBot.create(:account) }
  before do
    travel_to Time.new(2019, 10, 27, 10, 00, 00)
  end

These two before hooks get executed serially, so first the user gets created then the time is changed to the custom time.

So the lesson is that, if we want to execute some code in the before block which is going to impact even the creation of the data created in the specs, have it as your first before hook in the spec.

RSpec.describe Activity, type: :model do
  before do
    travel_to Time.new(2019, 10, 27, 10, 00, 00)
  end
  let!(:user) { FactoryBot.create(:user) }                                            
  let(:account) { FactoryBot.create(:account) }                                         
end

The tip of the day is that prefer having the before block as first block of the spec, even before defining let and let!.

Happy testing.

Discussion

pic
Editor guide