Skip to content

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