Why must a test's teardown still run after an assertion fails partway through the test?
answer
- The failing path skips code
- Placement decides whether cleanup runs
- After-each hooks run on both exits
- Teardown must survive a half-finished setup
- A killed process runs no hook at all
basics
~20 sA failing assertion aborts the test body, so cleanup written after it never executes and the records it created leak into later tests. Register teardown as an after-each hook or a resource-scoped block so it runs on both exits.
solid answer
~40 sAn assertion failure raises, so every statement after it in the test body is skipped, including the delete written at the bottom. The leftover records then change what the next test sees, and the failure that surfaces is in a different, innocent test. Put teardown somewhere the runner guarantees: an after-each hook, a resource-scoped block that unwinds on any exit, or a per-test transaction the harness rolls back regardless of outcome. Prefer a mechanism that resets before the next test runs rather than one that fires only once at the end of the class. Make teardown defensive too, because it may run when setup only half-completed: deleting a record that was never created must not itself throw and mask the original failure.
code
pseudocode · 21 lines# broken: the delete is skipped when the assertion raises
test "awards points":
member = createMember(tag = runTag)
awardPoints(member, 4)
assertEquals(4, balanceOf(member))
deleteMember(member)
# fixed: the runner owns the cleanup
beforeEach:
resetLedgerTables()
afterEach:
try:
deleteAllWhere(tag = runTag)
catch error:
log.warn("cleanup failed", error)
test "awards points":
member = createMember(tag = runTag)
awardPoints(member, 4)
assertEquals(4, balanceOf(member))go deeper
Be ready to say why cleanup written at the bottom of a test body never runs on the failing path, and to name the after-each hook or resource-scoped block you would use instead.
Explain the mechanics: what the runner guarantees about hook ordering, how a per-test transaction removes the need for an explicit delete, and why teardown must tolerate a setup that only half-completed.
Show the production judgement — resetting before rather than only after so a killed run self-heals, keeping a teardown error from masking the assertion, and naming the shared state a delete cannot reach.
Own the policy question: whether isolation is enforced by a harness the whole suite inherits or left to each author's discipline, and what suite run time you are willing to spend to make it a guarantee.
### The failing path is the path that matters A test method is ordinary code. When an assertion fails it raises, and everything below it in the same block is skipped, exactly as an unhandled error skips the rest of any function. So a test written as *create data, act, assert, delete data* has two exits: the passing exit runs the delete, the failing exit does not. The interesting runs are the failing ones, which means the cleanup you wrote is precisely the cleanup that does not happen when you need it. The consequence is not a lost record, it is a lost diagnosis. Take a suite over a loyalty-points ledger. Case 317 awards 4 points to a member and asserts the balance is 4; a bug makes it 6, the assertion raises, and the member row plus two ledger entries survive. Case 318 creates its own member, awards points, and asserts that a balances report lists exactly one member with a non-zero balance. It now sees two and fails. The report is correct. Case 318 is correct. But the pipeline reports a failure against 318, and whoever reads it starts by reading reporting code. One honest failure has become a second, misleading failure, and the misleading one is usually the one people chase first. ### Where cleanup has to live Three placements survive a raised assertion: 1. **An after-each hook registered with the runner.** The runner invokes it around the test body, so it runs on the passing and failing paths alike. 2. **A resource-scoped block** whose unwind is guaranteed on every exit from the block, however the language spells that construct. 3. **A per-test transaction the harness opens and unwinds** regardless of outcome, so there is no explicit delete to skip in the first place. Two placements do not survive: statements at the bottom of the test body, and any cleanup that sits behind an earlier assertion. An after-class or after-suite hook does run, but it runs too late to protect the next test in the class; it limits how far the pollution spreads without preventing it. ### Teardown must tolerate a half-built world Setup fails too. If the member was created but the ledger entry was not, teardown runs against a world that only half exists, and cleanup written as "delete the ledger entry whose id I remembered" throws on an identifier that was never assigned. Write teardown so that finding nothing to delete is success: delete by a per-run marker rather than by remembered identifiers, and treat a zero-row delete as normal rather than as an error worth raising. ### Do not let teardown eat the failure A teardown that throws while the test is already failing can replace the assertion message with its own. The reader then sees a complaint about a delete and never learns which behaviour broke. Many runners attach cleanup errors as secondary or suppressed failures so both survive; where yours does not, catch inside teardown, log enough to debug it, and let the assertion be the headline. The rule is that cleanup is infrastructure and must never outrank the finding. ### Clean before, not only after No hook runs at all when the process is killed. A cancelled pipeline, a machine reboot, or an out-of-memory kill part-way through a 27-minute suite leaves behind everything written up to that point, and the *next* run inherits it. That is why the sturdier pattern is to reset before each test rather than only after: a before-each reset guarantees a known starting point even when the previous run ended violently. Suites that can afford both do both, using the before-hook for correctness and the after-hook to keep a shared environment from growing without bound. If you can only afford one, put it before. ### What per-test cleanup cannot reach Records are the easy part. A delete does not reset a sequence counter, so identifiers keep climbing and any assertion on a specific one is already fragile. It does not clear an in-process cache warmed by an earlier test, a static configuration value someone mutated, a message left undelivered on a queue, a file dropped in a shared directory, or state created in a third-party sandbox account. Recognising which of these your suite actually touches, and resetting each through its own mechanism, is what separates a suite that is merely tidy from one that is genuinely independent. ### The escalation Hand-written per-test cleanup is the weakest form of isolation, because it depends on every author remembering it in every test. The stronger forms move the guarantee into shared infrastructure: a base harness the whole suite inherits, a per-test transaction that unwinds automatically, or a reset derived from the live schema rather than from a list someone maintains. Interviewers ask the teardown question because the answer reveals whether a candidate thinks of isolation as a habit or as a property the harness enforces.
- Your teardown itself throws while the test is already failing an assertion. What do you want the report to show?Both, with the assertion failure as the headline. A teardown exception that replaces the assertion message sends the next reader chasing a failed delete instead of the behaviour that broke. Most runners attach cleanup errors as secondary or suppressed failures; if yours does not, catch inside teardown, log the detail, and let the assertion surface. Teardown that tolerates partial setup usually avoids throwing in the first place.
- Is cleaning up after each test enough, or should the suite also reset before each test?Resetting before is more robust, because it also recovers from a run that was killed mid-test by a cancelled pipeline, a timeout, or a crash, when no hook ran at all. Many suites do both: a before-each reset guarantees a known starting point, and after-each cleanup keeps a shared environment from filling up. If you can afford only one, put it before.
- When does per-test cleanup stop being the right tool at all?When the leftover state is not record-shaped. A sequence counter, an in-process cache, a mutated global setting, an undelivered queue message, a file in a shared directory, or state in a third-party sandbox account are all untouched by a delete. Each needs resetting through its own mechanism, or isolating per run, and some values, such as a monotonic identifier, are simply not worth asserting on.
Cleanup at the bottom of a test body is like planning to lock the door on your way out. It works every time you leave normally, and never on the day you leave in a hurry.
saying these in an interview costs you the question
- Puts the delete at the bottom of the test body
- Assumes a failing test leaves no data behind
- Lets a teardown exception hide the real assertion failure
- Writes teardown that throws when setup half-completed
- Cleans up only once, after the whole class has run
- Relies on the next test overwriting the leftover records