skip to content

unittest (Standard Library)

Python's built-in xUnit framework — subclassing TestCase, the assertion methods, setUp and cleanup hooks, command-line discovery. Teams on other runners still inherit unittest suites, so it is asked.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

25

How do you assert in unittest that a block of code emitted a WARNING log record?

level: juniorimportance: must knowfreq 45%

answer

  1. Proving that code logged something
  2. A with block, not a callable
  3. Silence inside the block is failure
  4. Two views: objects and formatted lines
  5. Default level is INFO

basics

~10 s

Wrap the call in with self.assertLogs("my.logger", level="WARNING") as captured:. The block fails unless at least one record is emitted, and afterwards captured.records holds the LogRecord objects and captured.output the formatted strings.

solid answer

~40 s

`unittest.TestCase.assertLogs` is a context manager: `with self.assertLogs("thumbs.resize", level="WARNING") as captured:` runs the code, and the assertion **fails if no record at that level or above was emitted** on that logger or any of its children. Afterwards the object it yields exposes two views of the same capture: `records`, a list of `logging.LogRecord` objects you can inspect field by field (level number, logger name, the message arguments), and `output`, the same records rendered with the fixed format `"%(levelname)s:%(name)s:%(message)s"`. The first argument may be a logger name or a `logging.Logger`; when `level` is omitted it defaults to INFO. Assert on `records` rather than on `output` so the test checks the level and the payload rather than the exact wording. The negative form, `assertNoLogs`, was added in Python 3.10 and yields nothing.

code

python · 28 lines
python
import logging
import unittest

log = logging.getLogger("thumbs.resize")


def resize(width):
    if width > 4096:
        log.warning("width %d exceeds the cap", width)
    return min(width, 4096)


class ResizeTest(unittest.TestCase):
    def test_over_cap_warns(self):
        with self.assertLogs("thumbs.resize", level="WARNING") as captured:
            resize(4097)
        self.assertEqual(len(captured.records), 1)
        self.assertEqual(captured.records[0].levelno, logging.WARNING)
        self.assertIn("exceeds the cap", captured.records[0].getMessage())
        self.assertEqual(captured.output, ["WARNING:thumbs.resize:width 4097 exceeds the cap"])

    def test_at_cap_is_silent(self):
        with self.assertNoLogs("thumbs.resize", level="WARNING"):
            resize(4096)


if __name__ == "__main__":
    unittest.main()

go deeper

for a junior

Recall the shape: with self.assertLogs("name", level="WARNING") as captured:, then assert on captured.records. Remember that a block which logs nothing fails the test, and that the level defaults to INFO.

for a middle

Explain the two views the context manager yields, why records beats output, that a name or a logging.Logger may be passed, and that records from child loggers are captured because they propagate to the named logger's handler.

for a senior

Show that you check levelno as well as the text, that you pass structured data through extra= so assertions do not depend on wording, and that you know the block overrides the logger's handlers, level and propagation for its duration.

for a principal

Own the policy question: which log lines are contract and therefore worth a test, and which are debugging noise. Argue for asserting on structured record fields over prose so that log-message edits never become a build-breaking change.

### What the assertion is for Logging is real behaviour: an operator's alert, an audit line, a deprecation notice. Testing it with an ad-hoc handler that you attach and remember to detach is fiddly and leaks state between tests, so `unittest` ships the capture as an assertion. `unittest.TestCase.assertLogs(logger=None, level=None)` is **only a context manager** — unlike `assertRaises` or `assertWarns`, there is no callable form. Its contract has two halves, and interview answers usually miss the first one: 1. **It is an assertion.** If the block finishes and *nothing* was captured, the test fails with `no logs of level INFO or higher triggered on <name>`. An empty `with self.assertLogs(...)` block is a failing test, not a no-op. 2. **It is a capture.** The object bound by `as` carries `records` — a list of `logging.LogRecord` instances — and `output` — the same records formatted with the hard-coded `"%(levelname)s:%(name)s:%(message)s"`. ### Arguments The first argument is either a logger **name** (`"thumbs.resize"`) or a `logging.Logger` object; omit it and you get the root logger, which in a real test process also captures whatever any third-party library logs — almost always the wrong target. `level` accepts a name (`"WARNING"`) or a number (`logging.WARNING`) and **defaults to INFO**, not to WARNING and not to the logger's configured level. Records below the requested level are dropped by the capturing handler, so `assertLogs(level="ERROR")` around code that only logs a warning fails with the "no logs triggered" message. ### What it does to the logger Inside the block the named logger is temporarily rewired: its handler list is **replaced** by a single capturing handler, its level is set to the requested level, and its propagation flag is turned off so the records do not also reach the root handlers and spray your test output. All three are restored on exit, including when the body raises. Two consequences follow. First, the test does not depend on how logging is configured in the application — `assertLogs` forces the level, so a logger left at ERROR in production config still yields INFO records inside the block. Second, and for the same reason, `assertLogs` **cannot** prove that a given record would actually be visible in production; it proves the call site emitted it. Because the handler hangs on the *named* logger, records emitted on descendants are captured too — `assertLogs("thumbs")` sees a record logged on `thumbs.resize`, because the child propagates up to it. That is the idiomatic way to assert "this subsystem said something" without pinning the exact logger. It fails only when a descendant has its own propagation switched off. ### Assert on the records, not on the strings The common mistake is `self.assertEqual(captured.output, ["WARNING:thumbs.resize:width 4097 exceeds the cap"])`. It works, and it welds the test to the exact format string and to argument interpolation, so a harmless copy edit breaks it. Prefer the record: ```python record = captured.records[0] self.assertEqual(record.levelno, logging.WARNING) self.assertIn("exceeds the cap", record.getMessage()) ``` `getMessage()` is what performs the deferred `%`-style interpolation — logging does not format the message until something asks for it, which is why `record.msg` is still the raw template and `record.args` still the tuple. Any key you passed through `extra=` becomes a plain attribute on the record, which makes structured assertions (`record.elapsed_ms`) both precise and wording-independent. The level is the field people forget: asserting only on the text passes happily when a bug demotes an error to a debug line. Checking `levelno` is the whole reason to look at records at all. ### The negative form `assertNoLogs(logger=None, level=None)`, added in **Python 3.10**, is the mirror image: it fails if *any* record at that level or above appears, and it yields `None`, so `as cm` gives you nothing to inspect — there is nothing to inspect, by definition. Before 3.10 people hand-rolled it, usually by asserting that `assertLogs` itself raised `AssertionError`, which is a trick to recognise in old code rather than to write today. ### Where it stops `assertLogs` captures via the `logging` package only. A library that writes to `sys.stderr` directly, or issues a `warnings.warn` call, produces nothing for it — warnings are `assertWarns`'s job, and stream output belongs to `--buffer` or a redirect. Keep the two mechanisms distinct in your head; conflating them is the second-most-common answer failure after forgetting that an empty block fails.

  • What happens if the code inside a `with self.assertLogs(...)` block logs nothing at all?
    The test fails on exit with `no logs of level <LEVEL> or higher triggered on <logger>`. `assertLogs` is an assertion, not a passive recorder — an empty block is a failure, which is exactly why you cannot use it to express "this must stay quiet". Use `assertNoLogs` (Python 3.10+) for that.
  • Does `self.assertLogs("thumbs")` capture a record logged on `thumbs.resize`?
    Yes. The capturing handler is attached to the named logger, and a child logger's records propagate up to its ancestors' handlers, so anything under `thumbs.` is captured. The exception is a descendant whose own propagation has been switched off — those records never reach the ancestor's handler.
  • Why prefer `records` over `output` when writing the assertion?
    `output` is a rendered string, so asserting on it pins the test to the message's format string and to the interpolated arguments, and it silently ignores the level unless you parse the prefix. `records` gives `logging.LogRecord` objects, so you can assert `levelno` separately, call `getMessage()` for a substring check, and read any attribute passed via `extra=`.

It is a wiretap with a warrant condition attached: for the duration of the block every call on that logger is recorded, and if the line stays silent the test itself fails.

saying these in an interview costs you the question

  • Thinking an empty assertLogs block simply passes
  • Believing assertLogs also captures warnings.warn calls
  • Assuming the default capture level is WARNING
  • Asserting only on the formatted string, never on the level
  • Calling assertLogs with a callable instead of a with block
  • Defaulting to the root logger and capturing every library's output

context

open as a page

How do you run a single unittest test method from the command line with `python -m unittest`?

level: juniorimportance: must knowfreq 55%

basics

~10 s

Pass its dotted path as an argument: python -m unittest package.module.ClassName.test_method. Shorten the path to run a whole module or one TestCase, and a file path such as tests/test_csv_import.py works too.

open as a page

What do setUp and tearDown do in a unittest.TestCase, and when does each run?

level: juniorimportance: must knowfreq 72%

basics

~10 s

unittest calls setUp before every test_ method and tearDown after it, on a brand-new TestCase instance built per method. setUp prepares that test's state; tearDown releases it. If setUp raises, tearDown is skipped.

open as a page

What does @unittest.skip do to a test, and why prefer it to commenting the test out?

level: juniorimportance: must knowfreq 40%

basics

~20 s

The @unittest.skip decorator stops the decorated test body from running and makes the runner report it as skipped, with the reason you supplied. The test is still collected and still counted, so the gap stays visible in the summary; a commented-out test disappears entirely.

open as a page

What does unittest's TestCase.subTest do inside a loop of test cases?

level: juniorimportance: must knowfreq 50%

basics

~20 s

unittest.TestCase.subTest is a context manager that wraps one iteration of a loop, so a failure inside it is recorded and the loop keeps going instead of the first failed assertion ending the whole test method.

open as a page

How do you write a test with unittest.TestCase, and why prefer self.assertEqual to a bare assert?

level: juniorimportance: must knowfreq 72%

basics

~20 s

Subclass unittest.TestCase and give each test method a name starting with test. Inside, call assertion methods on self rather than the assert statement: they build a real failure message, and unlike assert they survive python -O.

open as a page

Why does an IsolatedAsyncioTestCase test that forgets an await still pass?

level: middleimportance: must knowfreq 52%

basics

~20 s

Calling an async def function without awaiting it runs none of the body — it returns a coroutine object, which is truthy and not None. So assertTrue succeeds, and the never-awaited RuntimeWarning only fires later, at garbage collection.

open as a page

Why does `python -m unittest discover` report zero tests in a directory full of `test_*.py` files?

level: middleimportance: must knowfreq 45%

basics

~10 s

Usually the directory has no __init__.py, so discovery refuses to recurse into it and silently finds nothing. The other common cause is a file name that does not match the default test*.py pattern.

open as a page

What does unittest.IsolatedAsyncioTestCase do that unittest.TestCase cannot?

level: juniorimportance: should knowfreq 40%

basics

~20 s

unittest.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.

open as a page

Why does unittest's assertWarns see a warning that a plain catch_warnings block misses?

level: middleimportance: should knowfreq 35%

basics

~20 s

Because warnings deduplicates. The default filter shows a given warning once per source location and remembers that in each module's __warningregistry__. assertWarns clears those registries and installs an "always" filter, so the warning is seen on every run.

open as a page

Why does self.addCleanup still run when a unittest setUp raises halfway through?

level: middleimportance: should knowfreq 46%

basics

~10 s

addCleanup registers a callable the moment you call it, and unittest drains that stack after every test - even one whose setUp died partway. tearDown, by contrast, is skipped entirely when setUp raises.

open as a page

What does @unittest.expectedFailure do, and what happens if that test passes?

level: middleimportance: should knowfreq 22%

basics

~20 s

The @unittest.expectedFailure decorator runs the test and inverts its verdict: a failure or error is recorded as an expected failure and does not break the run. If the test unexpectedly passes, it is recorded as an unexpected success and the run is reported as failed.

open as a page

When is @unittest.skipIf's condition evaluated, and when must you call self.skipTest instead?

level: middleimportance: should knowfreq 30%

basics

~20 s

A skipIf condition is an ordinary expression evaluated once, when the decorator line executes at import time — before any test or its setup runs. Use self.skipTest(reason) inside setUp or the test body whenever the decision depends on state that only exists at run time.

open as a page

In unittest, how does TestCase.subTest label which case failed?

level: middleimportance: should knowfreq 40%

basics

~10 s

Every keyword argument passed to unittest's TestCase.subTest, plus an optional leading message, is echoed in the failure header beside the test id, so each entry names the exact case instead of only the method.

open as a page

How does unittest.TestCase.assertAlmostEqual compare floats, and when do you pass delta?

level: middleimportance: should knowfreq 38%

basics

~20 s

assertAlmostEqual rounds the difference between the two values to seven decimal places and checks it is zero — an absolute tolerance. places= changes the digit count; delta= gives an explicit absolute bound instead. Passing both raises TypeError.

open as a page

Why does unittest's assertCountEqual pass where assertEqual fails on two lists?

level: middleimportance: should knowfreq 40%

basics

~20 s

assertCountEqual compares two iterables as multisets: same elements with the same multiplicities, order ignored. assertEqual on lists uses list equality, which is positional, so a reordered list fails even though both hold exactly the same items.

open as a page

With unittest's assertLogs and assertNoLogs, how do you test that a worker warns above its budget but not at it?

level: seniorimportance: should knowfreq 30%

basics

~10 s

Write both sides of the boundary: assertLogs(logger, level=logging.WARNING) around the budget+1 case, checking levelno and a structured field on the record, and assertNoLogs(logger, level=logging.WARNING) around the exactly-at-budget case.

open as a page

Why do IsolatedAsyncioTestCase tests fail after the first when a webhook-receiver suite shares one async client created in setUpClass?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Each IsolatedAsyncioTestCase test method gets a new event loop, closed when the test ends, and setUpClass cannot await anything. An async object built for the first test stays bound to that dead loop, so the second test raises RuntimeError.

open as a page

A unittest test passes alone but fails under `python -m unittest discover` — how do you diagnose it?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Discovery imports every test module into one process, so the suite shares module-level state. Reproduce by running the two modules together by dotted name, then bisect with -k, -f and -v until the polluting pair is isolated.

open as a page

Your unittest setUp rebuilds a metrics-scraper fixture with a 45-second cold start per test - when is setUpClass the right fix?

level: seniorimportance: should knowfreq 40%

basics

~20 s

setUpClass 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.

open as a page

A unittest suite reports 40 skips on CI and none locally. How do you find what silently stopped running?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Run the suite verbosely so every skip prints next to its reason, then trace each reason back to the condition that produced it. Skips are almost always import-time environment checks, so compare the CI image's platform, environment variables and optional capabilities against the local machine.

open as a page

Why can a unittest TestCase.subTest loop leak state between its cases?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Because subTest is a reporting boundary, not an isolation boundary: setUp, tearDown and cleanup callbacks bracket the whole test method, so anything a case mutates — instance attributes, module globals, caches, the filesystem — is still there for the next case.

open as a page

Why can a unittest self.assertRaises block pass for the wrong reason, and how do you tighten it?

level: seniorimportance: should knowfreq 42%

basics

~20 s

A self.assertRaises block asserts only that some statement inside it raised that class or a subclass — not which line or which call. Setup code inside the block can keep the test green while the code under test never runs.

open as a page

How do you build and run a unittest suite programmatically with TestLoader and TextTestRunner?

level: middleimportance: nice to knowfreq 20%

basics

~10 s

Ask a unittest.TestLoader for tests, collect them in a unittest.TestSuite, and hand that to unittest.TextTestRunner().run(suite). The returned result object's wasSuccessful() gives you the exit status.

open as a page

Why can a failure inside unittest's TestCase.subTest still abort the loop?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

Three escapes exist: code outside the with block is unguarded, fail-fast mode stops the method at the first bad case, and a result object without an addSubTest method makes subTest degrade to a plain pass-through so the first failure propagates.

open as a page