RSpec and `let!`: Understanding the Potential Pitfalls

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
Reason 1: let! is actually a precondition for a test and it hides it
In 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?
Reason 2: It hides the order of execution of preconditions
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
A Minitest equivalent
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
👉 If you like this article and want it in your inbox each week, subscribe to my newsletter. You’ll find ideas on Ruby, software development, software testing, building products and workshops, plus notes on creativity, tech trends, and whatever else sparks my curiosity.
👐 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.




