The RSpec Rollback Boundary

The transaction that undoes your test data doesn't start where you think it does.

1 The safety net you were promised

the transaction everything your test does in here gets undone when the test ends ๐Ÿงช one test

config.use_transactional_fixtures = true wraps each test in a database transaction, then rolls it back. That's the theory.

2 But the box doesn't start where you'd guess

A describe block is just Ruby code that defines your tests. It runs once, up front, while RSpec is still figuring out what tests exist — before any test, and its transaction, has begun.

describe block loads no transaction yet first "it" example starts transaction opens here example ends rolled back

3 Same-looking code, two very different outcomes

describe "my tests" do FactoryBot.create(:plan) outside any "it" it "some test" do ... end INSERT runs before BEGIN โ†’ COMMIT, stays in the DB

Created in the describe block

Ruby evaluates this line while RSpec is still building the example list — no transaction has opened.

describe "my tests" do BEGIN / SAVEPOINT it "some test" do FactoryBot.create(:plan) ROLLBACK โ†’ gone after the test

Created inside "it" (or via let!)

This line only runs once the example itself is executing — already inside the open transaction.

let! looks like it lives in the describe block, but it's really a before hook in disguise: RSpec runs it at the start of each example, after the transaction has already opened. That's why let! data rolls back, but a bare line in describe does not.

4 The rule of thumb

Ask one question about any line that creates data: does it run while an example is executing, or before RSpec has even started one?

Rolls back

Code inside an it block, or reached via let/let!, since both only run once an example is under way.

Does not roll back

Code written directly in a describe block, run at load time, before the first example's transaction has opened.