What does unittest.IsolatedAsyncioTestCase do that unittest.TestCase cannot?
answer
- Plain TestCase never awaits anything
- Something has to drive the coroutine
- Isolation is per test method
- asyncSetUp and asyncTearDown bracket the body
- A fresh loop, closed afterwards
basics
~20 sunittest.IsolatedAsyncioTestCase gives every test method its own fresh asyncio event loop and awaits async def test methods on it, along with asyncSetUp and asyncTearDown. A plain TestCase just calls the method and drops the coroutine it gets back.
solid answer
~40 s`unittest.TestCase` knows nothing about coroutines: it calls the test method, gets a coroutine object back and discards it, so the body never runs and the test reports success. `unittest.IsolatedAsyncioTestCase`, in the stdlib since Python 3.8, owns an event loop instead. For each test method it creates a new loop, runs the synchronous `setUp`, awaits `asyncSetUp`, awaits the `async def test_` method, awaits `asyncTearDown`, runs the synchronous `tearDown`, then unwinds cleanups — `addCleanup` and `addAsyncCleanup` share one LIFO stack — and finally cancels leftover tasks and closes the loop. *Isolated* is the load-bearing word: the loop is per test method, not per class or module, so nothing bound to one test's loop survives into the next. Plain `def test_` methods are still allowed on the class and run synchronously.
code
python · 18 linesimport asyncio
import unittest
class TestWebhookQueue(unittest.IsolatedAsyncioTestCase):
async def asyncSetUp(self):
self.deliveries = asyncio.Queue()
await self.deliveries.put("evt-1")
async def test_receives_one_delivery(self):
self.assertEqual(await self.deliveries.get(), "evt-1")
async def asyncTearDown(self):
self.assertTrue(self.deliveries.empty())
if __name__ == "__main__":
unittest.main()go deeper
Be ready to name unittest.IsolatedAsyncioTestCase as the base class you subclass when a test needs await, and to say plainly that a normal TestCase would never execute the body of an async def test method.
Explain the per-test sequence out loud: a fresh event loop, synchronous setUp, awaited asyncSetUp, the awaited test, asyncTearDown, synchronous tearDown, then cleanups popped LIFO before the loop closes.
Show what the isolation buys and costs. Leaked tasks are cancelled with the loop, but nothing loop-bound can be shared between tests, and there is no async setUpClass. Expect a probe on what survives when asyncSetUp raises.
Own the tradeoff between per-test isolation and suite runtime: which resources are worth rebuilding for every test, which expensive loop-free state can safely live at class scope, and what your house rule about async test isolation should say.
### Why the class exists at all `unittest.TestCase` is older than coroutines, and its runner treats a test method as an ordinary callable: it calls the method and ignores what comes back. Declare `async def test_x` on a plain `TestCase` and calling it merely *builds* a coroutine object — the body never executes, no assertion inside it ever runs, and the test is reported green. Since Python 3.11 unittest at least notices the smell and emits a `DeprecationWarning` saying the test returned a non-`None` value, with a hint to use `IsolatedAsyncioTestCase` as the base class, but the test still counts as a pass. `unittest.IsolatedAsyncioTestCase` (stdlib since Python 3.8) is the subclass that knows how to *drive* a coroutine. Subclass it instead of `TestCase` and each `async def test_` method is genuinely awaited. ### What happens around one test method `unittest` constructs a fresh instance of the case class for every test method, so the whole sequence below runs once per test: 1. A brand-new event loop is created. Since 3.11 the class drives the test through an `asyncio.Runner`, and it deliberately runs that loop in **debug mode** — which is why a never-awaited warning raised inside such a test arrives with a "Coroutine created at" traceback for free. 2. The synchronous `setUp` runs, with the loop already installed as the current one. 3. `asyncSetUp` is awaited. 4. The test method runs — awaited if it is `async def`, simply called if it is a plain `def`. Both are legal on this class, so one case class can hold a mix of sync and async tests. 5. `asyncTearDown` is awaited. Like the synchronous `tearDown`, it is **skipped** when `asyncSetUp` raised. 6. The synchronous `tearDown` runs. 7. Cleanups unwind. `addCleanup` and `addAsyncCleanup` push onto one stack and pop LIFO; the async ones are awaited, the sync ones called. `await self.enterAsyncContext(cm)` enters an async context manager and registers its `__aexit__` on that same stack. Cleanups run **even when `asyncSetUp` failed**, which is why "register the release the instant you acquire the resource" beats "release it in `asyncTearDown`". 8. The loop is shut down: tasks still pending are cancelled and given a chance to unwind, asynchronous generators are closed, then the loop is closed. ### What "Isolated" is promising The isolation is per **test method**, not per class and not per module. Two things follow. First, a task you forgot to await or cancel cannot leak into the next test — it dies with the loop. Second, and this is the part that bites in real suites, any object that captured that loop is dead too: an `asyncio.Event`, `Queue`, `Lock` or `Condition` binds to the running loop the first time it actually has to wait and refuses to be used from a different one afterwards, while streams, transports, subprocesses and background tasks hold their loop outright. Loop-bound state therefore has to be built inside `asyncSetUp` or the test itself, never shared at class scope. There is also no asynchronous equivalent of `setUpClass`: class-level hooks stay synchronous, because no loop is running when they execute. ### What it does not do It does not parallelize anything — tests still run one after another. It does not await your assertions for you, and it will not turn a forgotten `await` inside a test body into a failure; you get a passing test and a warning. And it does not replace `asyncio.run`: never call `asyncio.run` inside a test method on this class, because a loop is already running there and `asyncio.run` raises `RuntimeError` in that situation. ### How to answer in an interview Say what plain `TestCase` does with a coroutine (drops it), name the class, then give the order — sync `setUp`, `asyncSetUp`, the awaited test, `asyncTearDown`, sync `tearDown`, LIFO cleanups, loop closed — and finish on the isolation guarantee: one loop per test method. That is the whole mechanism, and reciting the order is what separates a candidate who has used it from one who has read about it.
- If asyncSetUp raises halfway through, which of asyncTearDown and the registered cleanups still run?`asyncTearDown` is skipped, exactly as the synchronous `tearDown` is skipped when `setUp` raises. Anything already registered with `addCleanup`, `addAsyncCleanup` or `enterAsyncContext` still runs, in LIFO order, before the loop closes. That asymmetry is the practical argument for registering a release the moment you acquire a resource rather than pairing acquisition in `asyncSetUp` with release in `asyncTearDown`.
- Can you still write a plain def test_ method on an IsolatedAsyncioTestCase subclass?Yes. The class calls a non-coroutine test method directly and awaits a coroutine one, so a single case class can hold both. The loop is still created and closed around the synchronous test, which costs a little time but keeps the lifecycle uniform. It is a normal way to keep a handful of pure-sync assertions next to the async ones they relate to.
- Why should you never call asyncio.run inside an IsolatedAsyncioTestCase test method?The class is already running a loop in that thread, and `asyncio.run` refuses to start another one — it raises `RuntimeError`. Even if it worked, the coroutine would execute on a throwaway loop with no relationship to the test's own, so anything it created would be unusable in the rest of the test. Inside the test you simply `await`.
saying these in an interview costs you the question
- Thinks a plain TestCase awaits async def test methods
- Believes one event loop is shared by the whole class
- Calls asyncio.run inside an async test method
- Expects asyncTearDown to run after asyncSetUp raised
- Thinks a sync def test_ method is illegal on the class