Using let_it_be
let_it_be comes from the test-prof gem. It creates the record once per example group and reuses it across examples.
That is great for speed, but it has a few sharp edges that can surprise you if you treat it like let.
When it is a good fit
- A context has many examples and the data can be shared safely.
- Records are read-only (or you can reset state between examples).
- You want to reduce database setup time without changing test behavior.
Caveats and pitfalls
Eager evaluation
let_it_be behaves like let! and is evaluated once when the example group is loaded
(right after before(:context)). It is not lazy and not per-example. Check out "Order of execution" at the bottom.
Mutations and cached associations
If an example mutates the record (or cached associations) without persisting to the DB,
use refind: true so each example gets a fresh reload.
let_it_be(:user, refind: true) { create(:user) }
before do
Role.non_member.add_permission!(:view_issues)
end
Declaration order matters
let_it_be does not support late initialization. If you reference another let_it_be
inside the block (for example tracker: bug), the referenced let_it_be(:bug) must be defined
earlier in the file. That also means it can't used in cases where you change tracker in each example.
Time travel in around(:each)
around(:each) runs after let_it_be, so time travel helpers will not affect records
created in let_it_be. Prefer using an explicit time variable.
let(:current_time) { Time.zone.parse("2024-01-01 12:00:00") }
around do |example|
travel_to(current_time) { example.run }
end
let_it_be(:issue) { create(:issue, due_date: current_time + 1.day) }
Small groups may not benefit
If a context has only a few examples, the extra complexity is rarely worth it.
Prefer plain let/let! unless there is a clear performance gain.
What still works without changes
Mocks and stubs are not persistent across examples. If you stub methods on an object
created via let_it_be, those stubs are cleared after each example as usual.
Order of execution
before(:suite) # 1. Runs once at the very start
↓
before(:context) # 2. Runs once per describe/context block
↓
let_it_be { } # 3. Runs once per describe/context block (after before(:context))
↓
around(:each) - BEFORE # 4. Runs before each test
↓
before(:each) # 5. Runs before each test
↓
let! { } # 6. Evaluated for each test
↓
THE TEST RUNS # 7. Your `it` block executes
↓
after(:each) # 8. Runs after each test
↓
around(:each) - AFTER # 9. Runs after each test
↓
after(:context) # 10. Runs once after all tests in the group
↓
after(:suite) # 11. Runs once at the very end