A unittest.mock.patch started in setUp leaks into later tests; how do you guarantee the restore?
answer
- Symmetry that is not symmetric
- setUp can die halfway through
- tearDown does not always run
- Register the undo when you create the obligation
- addCleanup, not tearDown
basics
~10 sRegister the teardown at the moment you start the patch: call addCleanup(patcher.stop) right after patcher.start(). Cleanups run even when setUp raises partway, whereas tearDown is skipped entirely in that case.
solid answer
~40 s`start()` is the only form of `patch` with no scope attached — the decorator and the `with` block restore in a `finally`, but a started patcher is restored only when something calls `stop()`. Pairing `start()` in `setUp` with `stop()` in `tearDown` looks symmetric and is not, because `unittest` skips `tearDown` when `setUp` raises, so a patch started before the failure stays installed for the rest of the process. The fix is `self.addCleanup(patcher.stop)` on the line after `start()`: cleanups run last-in-first-out whether or not `setUp` completed, and `addClassCleanup` does the same for `setUpClass`. `patch.stopall()` is the blunt safety net for patchers whose cleanup was forgotten. Because a leaked `MagicMock` is truthy and answers every attribute, the symptom is not an error but an order-dependent pass, so randomize test order to surface it.
code
python · 18 linesimport shutil
import unittest
from unittest.mock import patch
class ExportTests(unittest.TestCase):
def setUp(self):
patcher = patch("shutil.disk_usage")
self.disk_usage = patcher.start()
self.addCleanup(patcher.stop)
def test_free_space_is_read(self):
self.disk_usage.return_value = (0, 0, 1024)
self.assertEqual(shutil.disk_usage("/"), (0, 0, 1024))
unittest.main(argv=["ignored"], exit=False)
print(shutil.disk_usage("/").free > 0)go deeper
Know that a patcher started with start() must be stopped, and that addCleanup is how a TestCase does it. You are not expected to reason about setUp failure paths yet, only to write the pairing correctly.
Explain the asymmetry: cleanups run whether or not setUp completed, tearDown does not run at all if setUp raises. Be able to describe what a leaked MagicMock does to later tests — truthy, auto-attributes, silently passing assertions.
Demonstrate the diagnosis: order-dependent green, bisecting the test order, identifying the leaker rather than the victim, and the habits that prevent it — addCleanup at the point of start, specced doubles, randomized ordering in CI.
Own suite-level isolation as a policy question: randomized order in CI, a ban on module-scope patching, whether class-level doubles are worth their setup savings, and what a leak costs when a green regression pack ships a silently wrong result downstream.
`patcher.start()` is the only form of `patch` with no scope attached. The decorator restores in a `finally`; the `with` block restores in `__exit__`; `start()` restores when — and only when — something calls `stop()`. A suite that starts patches in `setUp` and stops them in `tearDown` looks symmetric and is not, because `unittest` does not run `tearDown` when `setUp` raises. **The failure this produces.** Consider a nightly ETL export to a warehouse whose regression pack holds 340 cases. One test class patches a row-limit helper in `setUp` with `start()`, and stops it in `tearDown`. A later change makes the second statement of `setUp` raise for one case: the first patch is already active, `setUp` aborts, `tearDown` is skipped, and the replacement stays bound on the module for the rest of the process. Every subsequent test that reaches that helper gets a `MagicMock`. A mock is truthy, its attribute access always succeeds, and arithmetic against a `MagicMock` returns another `MagicMock` — so the tests that consume it do not error, they pass. The regression pack stays green while the export silently truncates to whatever the leaked double implies, and the truncation is found in the warehouse, not in CI. **The fix, and why it is the specific one.** Register the teardown at the moment the patch starts: ```python def setUp(self): patcher = patch("app.row_limit") self.row_limit = patcher.start() self.addCleanup(patcher.stop) ``` `addCleanup` callbacks are run in last-in-first-out order *whether or not* `setUp` completes, and they run after `tearDown` when `tearDown` does run. So the third patch started in a `setUp` that dies on the fourth line is still stopped. Nothing about the pattern is stylistic: it is the only arrangement where the restore is guaranteed by the same statement that created the obligation. For a patch established in `setUpClass`, `addClassCleanup` plays the same role. `patch.stopall()` stops every patcher started with `start()` and not yet stopped, and it is the blunt safety net — registered once as a cleanup, it catches patches whose own cleanup registration was forgotten. **Why the class-level lifetime is chosen at all.** It is usually for speed or for sheer repetition: a double that is expensive to construct, or a patch every one of forty methods needs. Both are real, but weigh them against the failure mode above. A decorator on the class, or a `with` block inside a helper method, buys the same reuse with an automatic restore, and the per-test cost of building a `MagicMock` is negligible. **Diagnosing a leak you already have.** The signature is order dependence: the suite is green in one order and red in another, or a test fails in the full run and passes alone. The mechanical process is a bisection over test order — run halves of the suite, then quarters, until you have the smallest pair where the second fails only after the first. The failing test names the *victim*; the leaker is upstream of it. Randomizing test order routinely, rather than only when something breaks, converts this class of bug from "found in production" to "found on the branch that introduced it". Two further habits catch it earlier. First, make the leak loud rather than silent: a double built with `create_autospec` or a `spec` raises `AttributeError` on an attribute the real object does not have, and a `PropertyMock` or a configured `side_effect` that raises is far less likely to sail through an unrelated test than a bare `MagicMock`. Second, assert on the double in the test that owns it — a test that never touches the mock it installed is a test that would not notice the patch failing either. **What does not help.** Waiting for garbage collection does nothing; the patcher holds the original and the target holds the mock, and neither is undone by collection. Stopping a patcher twice is not a fix either — a second `stop()` on an already-stopped patcher raises `RuntimeError`, so a belt-and-braces `stop()` in `tearDown` on top of `addCleanup` swaps one failure for another; register the cleanup and leave it alone. And a patch started at module import — outside any test, "so it is there for everything" — has no scope at all: it is active for the entire process including any code the runner imports afterwards, and no cleanup hook exists that could unwind it at the right moment. The rule that survives the interview: any `start()` without a paired, guaranteed `stop()` is a bug the day it is written, and the guarantee is `addCleanup`, not `tearDown`.
- Why is stopping the patch in tearDown not enough?`unittest` runs `tearDown` only when `setUp` completed. If `setUp` starts three patches and raises on the fourth line, `tearDown` never runs and those three stay installed for the remainder of the process. Cleanups registered with `addCleanup` before the failure still run, in reverse order, which is why the registration belongs immediately after each `start()` rather than in a separate method.
- What exactly does patch.stopall() stop, and when would you call it?Every patcher started with `start()` that has not yet been stopped, in reverse order of starting. It is a safety net rather than a primary mechanism — registered once as a cleanup, it catches patches whose own registration was forgotten. It does not touch decorator or context-manager patches, which manage themselves, and calling `stop()` again on an already-stopped patcher raises `RuntimeError`.
- How would you track down which test is leaking a patch?Bisect over test order. Run halves of the suite, then quarters, until you find the smallest pair where the second test fails only after the first — the failing test is the victim, the leaker is upstream. Randomized ordering in CI turns this from an archaeology exercise into a failure on the branch that introduced it, and specced doubles make the leak raise instead of quietly passing.
start() without a registered stop is a fire door propped open at the start of a shift: nothing goes wrong on the shift that propped it, and the incident happens to whoever walks through later.
saying these in an interview costs you the question
- Relies on tearDown that never runs when setUp raises
- Assumes garbage collection undoes an unstopped patch
- Blames the failing test rather than the leaking one
- Calls start() at module import for convenience
- Trusts that a leaked mock would fail loudly
- Adds a second stop() in tearDown as belt and braces