skip to content

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

level: juniorimportance: must knowfreq 72%

answer

  1. The class you inherit from
  2. How the runner knows a method is a test
  3. Why the message is richer than AssertionError
  4. assertEqual dispatches on argument type
  5. -O removes statements, not method calls

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.

solid answer

~40 s

A unittest test is a method whose name begins with `test` on a class that subclasses `unittest.TestCase`; the framework collects those methods, builds an instance per method and calls each one. Inside, use the assertion methods on `self` — `assertEqual`, `assertIn`, `assertIs`, `assertIsNone` — instead of the `assert` statement. They raise `TestCase.failureException`, so the runner counts a FAIL rather than an ERROR, and `assertEqual` dispatches on the argument types to produce a real diff: a unified diff for two strings, an element-level report for two lists or dicts. A bare `assert` gives you `AssertionError` with no values at all, and it is deleted at compile time under `-O`, so a suite built on it can pass while checking nothing. `msg=` is appended to the generated message, not substituted for it.

code

python · 19 lines
python
import unittest


def archive(messages):
    if not messages:
        raise ValueError("empty batch")
    return list(messages)


class TranscriptArchiveTest(unittest.TestCase):
    def test_archive_keeps_message_order(self):
        self.assertEqual(archive(["hi", "there"]), ["hi", "there"], msg="order must survive archiving")

    def test_archive_rejects_empty_batch(self):
        with self.assertRaises(ValueError):
            archive([])


unittest.main(argv=["tests"], exit=False, verbosity=2)

go deeper

for a junior

Be ready to type a TestCase subclass from memory: the import, the class, a method whose name starts with test, and an assertEqual call on self. Know that a misspelled prefix means the test silently never runs.

for a middle

Explain the mechanics: assertion methods raise failureException so the runner reports FAIL rather than ERROR, assertEqual dispatches on type to build a diff, and msg= appends because longMessage is True.

for a senior

Show the production angle — a suite of bare asserts is checking nothing under -O, and unreadable failure output costs real triage time. Know to set self.maxDiff = None when a comparison prints a truncated diff.

for a principal

Own the standard: which assertion vocabulary the codebase uses, whether custom types get an equality function registered via addTypeEqualityFunc so failures are readable, and how much diff output a CI log should carry.

### What a unittest test actually is `unittest` is the standard library's xUnit-style test framework. A test is a **method** on a class that inherits from `unittest.TestCase`, and whose name begins with the prefix `test`. The framework collects those methods, builds an instance of the class for each one, and calls it. Nothing else on the class is collected: helper methods, constants and properties are ignored. That single rule is behind the most common silent-suite bug in real code — a method named `check_archive_roundtrip` or `Test_archive` never runs, and the suite is green because it contains one fewer test than its author believes. ```python import unittest class ArchiveTest(unittest.TestCase): def test_batch_is_written_in_order(self): self.assertEqual(archive(["a", "b"]), ["a", "b"]) def helper(self): # not collected: no test prefix ... ``` A test passes if the method returns without raising. It **fails** if it raises the class held in `TestCase.failureException` (`AssertionError` by default), which is what every assertion method raises. It **errors** if it raises anything else. Both count against the run, but the runner reports them separately, and the distinction is diagnostic: a FAIL means your expectation did not hold, an ERROR means the code under test (or the test itself) blew up before the expectation was even reached. ### Why `self.assertEqual(...)` and not `assert a == b` Three reasons, in order of how often they bite. **1. The failure message.** `assertEqual` does not just compare; it dispatches on the type of the arguments through a registry of type-specific equality functions (extensible with `TestCase.addTypeEqualityFunc`). Two `str` values go to `assertMultiLineEqual`, which prints a unified diff with a caret under the differing character; two `list` values go to `assertListEqual`, two `dict` values to `assertDictEqual`, two `set` values to `assertSetEqual`, tuples to `assertTupleEqual`. You get told *what* differs. A bare `assert a == b` raises `AssertionError` with an empty message; you get the source line and nothing about the values. **2. `-O` deletes `assert` statements.** Running Python with `-O` sets `__debug__` to `False` and strips every `assert` statement at compile time. A suite built on bare asserts run under `-O` passes unconditionally — it checks nothing and says so nowhere. Assertion methods are ordinary method calls and are unaffected. This is why "use the framework's assertions" is a rule and not a style preference. **3. Intent.** Picking the assertion that names what you mean produces a better message for free. `assertIs(x, sentinel)` says identity and prints `... is not ...`; `assertIsNone(x)`, `assertIn(item, container)`, `assertIsInstance(obj, cls)` and `assertCountEqual(a, b)` all carry their own diagnostics. The anti-pattern is `assertTrue(a == b)`, whose entire failure message is `False is not true` — you have thrown the values away before the framework could show them. ### `msg=`, `longMessage` and `maxDiff` Every assertion method takes a trailing `msg=` argument. By default `TestCase.longMessage` is `True`, so `msg=` is **appended** to the generated message rather than replacing it — you keep the diff *and* get your explanation of why the values should have matched. Setting `longMessage = False` on the class restores the older replace-the-message behaviour, which is almost always a downgrade. Long diffs are truncated, and the truncation is deliberate: `TestCase.maxDiff` defaults to 640 characters and the message ends with a note telling you to set it to `None` to see the whole thing. When a test compares two large structures and prints an unusable stub of a diff, set `self.maxDiff = None` in that test. ### Choosing an assertion, and `self.fail()` `assertEqual` is the workhorse; `assertIs` is for identity (`None`, `True`, `False`, module-level sentinels) and never for value comparison, because two equal objects are frequently not the same object. `assertTrue`/`assertFalse` are for genuine booleans, not for smuggling an expression past the framework. When no assertion expresses the situation — an unreachable branch, a state machine that should never have got here — call `self.fail("message")` directly; it raises the same `failureException` with the message you wrote. ### Why the assertions live on `self` It is not decoration. The assertion methods read per-test state off the instance: `failureException` decides which class is raised, `longMessage` decides whether your `msg=` is appended or substituted, `maxDiff` decides how much of a diff survives, and the type-equality registry decides which comparison function `assertEqual` dispatches to. Setting `self.maxDiff = None` inside one test method therefore affects only that test, and registering a comparison function for your own value type makes every later `assertEqual` on it produce a readable diff instead of two `repr()` strings side by side.

  • What is the difference between a failure and an error in unittest's summary?
    A failure is an assertion that did not hold: the method raised `TestCase.failureException`, which is `AssertionError` by default and is what every assertion method raises. An error is any other exception escaping the test — a `TypeError`, an import problem, a bug in the test itself. Both count against the run, but the split tells you immediately whether your expectation was wrong or the code never got far enough to be checked.
  • Does passing msg= replace the generated failure message?
    No. `TestCase.longMessage` defaults to `True`, so `msg=` is appended to the message the assertion generated — you keep the diff and gain your explanation. Setting `longMessage = False` on the class switches to the old replace-it behaviour, which throws away the most useful half of the output; it is almost never worth doing.
  • Why is assertTrue(a == b) considered an anti-pattern?
    Because the comparison happens before the framework sees anything: `assertTrue` receives a single boolean, so its failure message is `False is not true` and both values are gone. `assertEqual(a, b)` receives the objects themselves and can dispatch on their type to show a diff. Reserve `assertTrue` for values that are genuinely booleans.
  • When would you call self.fail() instead of an assertion method?
    When no assertion expresses the situation — an unreachable `else` branch, a state machine that should never have reached this point, a callback that must never fire. `self.fail("message")` raises the same `failureException` with your text, so the runner reports it as a failure rather than an error.

A bare assert is a smoke alarm that only beeps; an assertion method is one that also tells you which room.

saying these in an interview costs you the question

  • Thinks every public method on the class is collected as a test
  • Uses bare assert and cannot say what -O does to it
  • Claims msg= replaces the generated diff
  • Writes assertTrue(a == b) instead of assertEqual
  • Says a failure and an error mean the same thing to the runner
  • Calls test methods directly instead of letting the runner do it

context