Verify Database Transaction Rollback
Assert that a failed multi-step write leaves no partial state, using a real database in the test rather than mocks.
Task
Test that a transaction rolls back completely when a later step fails.
When to use this
A partial write survives a failed operation, leaving inconsistent records that break reconciliation.
Prerequisites
- A real or containerised database in tests
- A repository layer using transactions
- A way to force a mid-transaction failure
Steps
1. Run the write with an injected failure in the second step.
await expect(service.transfer({ from: 'a', to: 'b', amount: 10, failAfter: 1 })).rejects.toThrow();Expected output: The operation rejects, signalling the transaction did not commit.
2. Assert both accounts are unchanged.
expect(await balance('a')).toBe(100);
expect(await balance('b')).toBe(50);Expected output: Neither the debit nor the credit persisted, proving atomic rollback.
Troubleshooting
Problem
Test database persists between runs
Solution
Wrap each test in a transaction that rolls back, or recreate schema per run.
Problem
Mocks hide the bug
Solution
Use a real database engine; mock repositories cannot prove transaction semantics.
Limitations
- Requires a real database in the test environment.
- Does not cover distributed transactions across services.
- Slower than in-memory doubles.
What this recipe does
Test that a transaction rolls back completely when a later step fails.
A partial write survives a failed operation, leaving inconsistent records that break reconciliation.
Prerequisites
- A real or containerised database in tests
- A repository layer using transactions
- A way to force a mid-transaction failure
Procedure
- Run the write with an injected failure in the second step.
await expect(service.transfer({ from: 'a', to: 'b', amount: 10, failAfter: 1 })).rejects.toThrow();
Expected result: The operation rejects, signalling the transaction did not commit.
- Assert both accounts are unchanged.
expect(await balance('a')).toBe(100);
expect(await balance('b')).toBe(50);
Expected result: Neither the debit nor the credit persisted, proving atomic rollback.
How to confirm it worked
Run the commands above and confirm every expected result in order. If a step does not produce the stated output, use the troubleshooting notes before continuing. A passing run shows after the injected failure both tables are byte-for-byte unchanged, and removing the transaction makes the test fail.
Troubleshooting
Test database persists between runs — Wrap each test in a transaction that rolls back, or recreate schema per run.
Mocks hide the bug — Use a real database engine; mock repositories cannot prove transaction semantics.
Limitations
- Requires a real database in the test environment.
- Does not cover distributed transactions across services.
- Slower than in-memory doubles.