Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Spring transactional tests: preemptive timeouts can escape rollback

Last updated: 5 Oct 20264 min read
tutorial
IntermediateBy AITrove Editorial

Spring binds a test-managed transaction to the test thread; a preemptive timeout that runs the body on another thread can commit real writes.

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

Java
@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.

spring
spring-testing
preemptive-test-timeout-transaction
Storage details