Your unittest setUp rebuilds a metrics-scraper fixture with a 45-second cold start per test - when is setUpClass the right fix?
answer
- Speed and isolation pull opposite ways
- Something runs once, not per test
- State lands on the class, not the instance
- Read-only sharing is nearly free
- Mutable sharing buys order-dependent tests
basics
~20 ssetUpClass runs once per class instead of once per test, removing the repeated cold start. Take it when the shared object is effectively read-only; if it holds buffers, counters or registries, sharing costs you test isolation.
solid answer
~50 s`setUpClass` is a `classmethod` called once before the class's first test, so twelve tests pay the 45-second cold start once rather than twelve times. What you spend is isolation: it assigns to `cls`, and the fresh-instance-per-test guarantee covers only attributes on `self`, never class attributes. If the scraper holds a collector registry, a sample buffer or counters, one test's mutations reach the next - the classic symptoms being a test that passes alone but fails in the suite, and duplicated side effects when a registration happens twice. So the rule is: share it if it is effectively read-only after construction; otherwise share only the expensive *subcomponent* and give each test a cheap disposable facade over it in `setUp`, or reset the mutable parts per test. Register the teardown with `addClassCleanup`, not `tearDownClass`, because a `setUpClass` that raises skips `tearDownClass` entirely.
code
python · 21 linesimport unittest
class ScraperTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.scraper = {"started": True}
cls.addClassCleanup(print, "scraper stopped once, after the class")
def setUp(self):
self.samples = []
def test_scraper_is_shared(self):
self.samples.append(1)
self.assertTrue(self.scraper["started"])
def test_samples_are_not_shared(self):
self.assertEqual(self.samples, [])
unittest.main(verbosity=0, exit=False)go deeper
Know that setUpClass runs once per class while setUp runs before every test, and that it is a classmethod assigning to cls. That difference alone explains most shared-state surprises you will meet.
Explain why class attributes escape the per-test isolation that instance attributes get, and name the failure signature: a test that passes alone and fails in the suite. Be able to write the addClassCleanup version correctly.
Show the judgement. Measure the cost, distinguish shared-immutable from shared-mutable, propose the middle path of an expensive shared core with a cheap per-test facade, and know that setUpClass runs once per process so single-test runs are not helped.
Own the policy on fixture scope for the whole suite: when widening scope is permitted, what must be provably immutable before it is shared, and how order-dependence is detected rather than tolerated. Suite trustworthiness outranks suite wall clock.
### What `setUpClass` actually changes `setUpClass` is a `classmethod` that unittest calls **once**, before the first test in that class runs; `tearDownClass` is its counterpart, called once after the last one. Anything it builds is assigned to `cls`, which means it is a **class attribute** - and class attributes are the one thing the per-test-instance guarantee does not protect. Each test method still gets its own `TestCase` instance, so `self.samples` from `setUp` is private; `cls.scraper` from `setUpClass` is shared by all of them. So the scenario is a straight trade. Twelve tests, each paying a 45-second cold start to stand up the metrics scraper, is nine minutes of wall clock spent constructing the same object twelve times. Move that construction to `setUpClass` and the suite pays it once: forty-five seconds total, an order-of-magnitude win. What you spend for it is isolation. ### What you are actually buying with the isolation Shared **immutable** state is nearly free. A parsed config, a compiled schema, a loaded fixture file, a warmed cache that is only read - none of those can carry information from one test to the next, and putting them in `setUpClass` is close to a pure win. Shared **mutable** state is where suites rot. The scraper is the awkward case: it holds a collector registry, a sample buffer and counters. Test A scrapes once and leaves a sample in the buffer; test B asserts the buffer holds one sample and passes - for the wrong reason. Test C registers a second collector against the shared registry, and from then on every scrape produces a duplicated side effect, each metric emitted twice, surfacing as a failure in a test that never touched the registry. The characteristic symptom is a test that passes alone and fails in the suite, or vice versa, and the characteristic bad fix is renaming methods so the default sorted-name ordering happens to work. ### Failure semantics you should be able to state - If `setUpClass` raises, every test in the class is reported as an **error** and none of them run; `tearDownClass` is **not** called. - Cleanups registered with `addClassCleanup` (3.8) **do** still run in that case, which is precisely why an expensive class-scoped resource should register its teardown with `addClassCleanup` rather than rely on `tearDownClass`. `enterClassContext` (3.11) does the same for anything `with`-shaped. - A class-scoped resource that leaks does not leak for one test; it leaks for the process. ### The decision procedure 1. **Measure first.** Forty-five seconds per test is a real number and justifies the conversation; "it feels slow" does not. 2. **Ask what the cost actually is.** Often the expensive part is a subcomponent - a connection, a compiled artifact, a warmed cache - not the whole object. Share that piece and keep the cheap wrapper per test. 3. **Ask whether the shared thing is mutable.** If it is read-only after construction, share it. If it carries buffers, counters or registries, either do not share it or explicitly reset those in `setUp`. 4. **Prefer a cheap per-test facade.** Build the expensive thing once in `setUpClass`, then in `setUp` hand each test a fresh lightweight object wrapping it - a new collector list, a scoped transaction rolled back afterwards, a fresh view over the shared data. This keeps the speed and most of the isolation. 5. **Pair it with `addClassCleanup`** so a half-built class resource is still released. 6. **Widen the scope only under protest.** Module-level `setUpModule()` and `tearDownModule()` functions run once for every class in the module and share state even more broadly; the wider the scope, the more the suite's result depends on the order it happened to run in. ### The costs people forget - **Discovery and per-process runs.** `setUpClass` runs once per class *per process*. Splitting a class across parallel workers re-pays the cold start in each, and running a single test by its dotted path still pays it in full - so the fast-iteration case is not helped as much as the full-suite number suggests. - **Debuggability.** A failure inside `setUpClass` errors an entire class at once, with no indication of which test would have caught the real problem. - **Ratchet effect.** Once state is on the class, the next engineer adds one more thing to it, and the isolation loss compounds quietly. ### The honest senior answer Move it to `setUpClass` when the object is expensive to build and safe to share - meaning effectively read-only or cheaply resettable - and register its teardown with `addClassCleanup`. When it is expensive *and* mutable, the right move is usually not a wider fixture scope but a cheaper construction: cache the costly subcomponent, make the shared part immutable, or give each test a disposable view over it. Trading correctness for wall clock in the test suite is the one place the trade is almost never worth it, because a suite you cannot trust costs more than the nine minutes.
- If setUpClass raises, what happens to the tests in that class and to tearDownClass?Every test in the class is reported as an error and none of them execute, and `tearDownClass` is not called at all. Cleanups registered with `addClassCleanup` do still run, which is why an expensive class-scoped resource should register its release there rather than depend on `tearDownClass` - otherwise a fixture that dies halfway leaks for the whole process.
- How would you keep the speed of a class-scoped fixture without losing per-test isolation?Share only the expensive, immutable part and rebuild the cheap mutable part per test. Construct the costly subcomponent once in `setUpClass`, then in `setUp` hand each test a disposable wrapper over it: a fresh collector list, a transaction rolled back in a cleanup, or a copy of the shared data. Tests then get an independent view while paying the cold start once.
- Does moving setup to setUpClass help when you run a single test during debugging?Barely. `setUpClass` runs once per class per process, so a single-test run still pays the full cold start, and parallel workers each pay it again for the classes they own. The win is concentrated in the full-suite run. If fast single-test iteration is the actual goal, the answer is a cheaper or faked construction, not a wider fixture scope.
setUp gives every test its own workbench; setUpClass gives the whole class one shared machine. Sharing a lathe that only cuts is fine - sharing one that keeps a bin of offcuts means every test inherits the last one's mess.
saying these in an interview costs you the question
- Treats setUpClass as a free speedup with no cost
- Assumes the fresh instance protects class attributes too
- Thinks tearDownClass still runs when setUpClass raised
- Relies on method ordering to sequence shared state
- Believes setUpClass runs once for the whole suite
- Moves mutable state to the class without resetting it