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. It speeds up specs, but it behaves differently from 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 behaviour.

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. See "Order of execution" below.

Mutations and cached associations

If an example mutates the record or its cached associations without persisting it, use refind: true so each example gets a fresh reload.

let_it_be(:user, refind: true) { create(:user) }

Declaration order

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 cannot be 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

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