This is not a new topic; various resources have addressed it in different ways. Here are my reasons and explanations for why I prefer not to use ‘let!’ in RSpec.
When I work on a project that uses RSpec, I prefer not to use let!. Instead, I call the let variable inside the before block.
RSpec.describe Thing do
let(:precondition) { create(:item) }
before
precondition
end
it 'returns that specific value' do
# do
# expect
end
end
Taking it a step further, if you do not need to reference precondition in your tests, you can do this instead:
RSpec.describe Thing do
before
create(:item)
end
it 'returns that specific value' do
# do
# expect
end
end
First what does let! do? It just a call to the normal let and then setting a before block for the same name.
# source: https://github.com/rspec/rspec/blob/main/rspec-core/lib/rspec/core/memoized_helpers.rb#L329
def let!(name, &block)
let(name, &block)
before { __send__(name) }
end
let! is actually a precondition for a test and it hides itIn testing let! acts as a test precondition: the required state of the system before running a specific test. This should be visible while reading the test and using ! for this makes it hard to spot and hard to load it in the mental context.
It is harder to notice a let! among several let statements when you are trying to debug than seeing a before block which clearly communicate intention to run some preconditions.
RSpec.describe Thing do
let(:account) { build(:account) }
let!(:organisation) { build(:organisation, account: account) }
it 'returns that specific value that we want' do
# test
end
end
compared with:
RSpec.describe Thing do
let(:account) { build(:account) }
let(:organisation) { build(:organisation, account: account) }
before
organisation
end
it 'returns that specific value that we want' do
# test
end
end
Which version makes it clearer that the organisation is created before each test?
Here is a comparison:
RSpec.describe Thing do
let!(:account_a) { build(:user, email: email) }
let!(:account_b) { build(:user, user: email2) }
let(:organisation) { build(:organisation, account: account) }
let(:team) { build(:team, account: organisation) }
it 'returns that specific value that we want' do
# test
end
end
compared to
RSpec.describe Thing do
let(:account_a) { build(:user, email: email) }
let(:account_b) { build(:user, user: email2) }
let(:organisation) { build(:organisation, account: account) }
let(:team) { build(:team, account: organisation) }
before
account_a
account_b
end
it 'returns that specific value that we want' do
# test
end
end
But imagine that after a series of changes there is a risk that someone might put a let! among other calls.
RSpec.describe Thing do
let!(:account_a) { build(:user, email: email) }
let(:organisation) { build(:organisation, account: account) }
let!(:account_b) { build(:user, user: email2) }
let(:team) { build(:team, account: organisation) }
it 'returns that specific value that we want' do
# test
end
end
Here are three ways to write something similar in Minitest:
class ThingTest < Minitest::Test
def setup
@account_a = build(:user, email: email)
@account_b = build(:account, user: user)
@organisation = build(:organisation, account: account)
end
def test_computed_slug_returns_with_dashes
@account_a.name = "My Name"
assert_equal "my-name", @account_a.computed_slug
end
end
Here is another approach:
class ThingTest < Minitest::Test
attr_accessor :account_a, :account_b, :organisation
def setup
@account_a = build(:user, email: email)
@account_b = build(:account, user: user)
@organisation = build(:organisation, account: account)
end
def test_computed_slug_returns_with_dashes
account_a.name = "My Name"
assert_equal "my-name", account_a.computed_slug
end
end
👐 Want to improve your developer testing skills? Visit goodenoughtesting.com/articles to discover resources on testing for developers.
👉 Join my Short Ruby Newsletter for weekly Ruby updates and visit rubyandrails.info, a directory of Ruby learning content.
🤝 Connect with me on Linkedin, Bluesky, Ruby.social, , and Twitter, where I mostly post about Ruby and Ruby on Rails.
🎥 Follow my YouTube channel for short videos about Ruby and Rails.