Spring binds a test-managed transaction to the test thread; a preemptive timeout that runs the body on another thread can commit real writes.
Spring transactional tests: preemptive timeouts can escape rollback
The rollback assumption breaks
A receipt integration test is annotated @Transactional and expects its insert to roll back after the method. A preemptive timeout helper may execute the assertion body in a separate worker thread. That worker does not inherit the test-managed transaction bound to the original thread, so service calls can open and commit their own transactions. The outer test rollback still occurs, but it rolls back the wrong thread’s empty work. Transaction thread ownership is the core issue.
Use a safe timing strategy
Prefer a non-preemptive timing assertion for a short, deterministic block, or use a test framework timeout mode that runs the test body on the same thread. If the operation truly hangs, enforce a database statement or client timeout at the component boundary rather than moving a transactional test body to an untracked thread. Verify the chosen JUnit timeout mode in the project version. Application transaction timeouts are separate from test runner deadlines.
Prove cleanup from outside
After the test completes, open a new connection and assert the unique receipt key is absent. This catches a committed write that in-test assertions miss. Do the check with a disposable database so a failure can be inspected safely. If a real HTTP request is involved, the server already runs on another thread, regardless of timeout style; live-server writes need deliberate cleanup.
Implementation contract
@Transactional
@Test
void receiptWriteRollsBackOnTestThread() {
assertTimeout(Duration.ofSeconds(7), () -> {
receiptService.issueReceipt(commandWithReference("r-47"));
});
}
// Avoid assertTimeoutPreemptively around this transaction-bound body.Cost and verification
A same-thread timing assertion cannot forcibly stop a hung operation, so component-level timeouts are still necessary. It preserves rollback semantics and avoids leaking committed test rows.
Common Mistakes
- Do not wrap a transactional test body in assertTimeoutPreemptively.
- Do not infer rollback from an assertion made before the test transaction ends.
- Do not assume @Transactional covers a live HTTP server request on another thread.
Read next
Spring imperative transaction: a worker thread does not inherit it, Spring Boot HTTP test: client rollback does not own server writes, Spring transaction timeout: put the deadline on the owning scope, Spring command rollback test: inspect state after an injected failure, Spring Testcontainers parallel tests: one database can leak another test's rows.
