Topic 11.5
unittest and doctest
In one line
unittest is the testing framework built into Python: tests are methods of a TestCase class with setUp and assertEqual-style checks. doctest runs the examples written inside docstrings, so documentation that shows >>> sessions stays true.
Think of it like this
Two ways to check a recipe book. unittest is a formal exam: a printed paper with a fixed layout, a marker, and a score sheet. doctest is a textbook whose worked examples check themselves: every time you build the book, a robot cooks each example and shouts if the picture in the book no longer matches the dish. Both ship with Python, so they need no install.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- `unittest`
- The testing framework in Python's standard library.
- `TestCase`
- The base class for unittest tests. Each
test_*method is one test. - `setUp` / `tearDown`
- Methods that run before and after every test method.
- Assertion method
- A
self.assertXxxmethod that checks something and fails the test with a clear message. - Test loader / runner / result
- The parts that find tests, run them, and record what passed or failed.
- `doctest`
- A module that runs the examples written inside docstrings.
- Docstring
- A string right under a
deforclassthat documents it (Topic 5.9). - Directive
- A special comment such as
+ELLIPSISthat changes how a doctest compares output. - Legacy suite
- An older, existing set of tests written in another style than the one you now prefer.
Step by step
01A TestCase class
The class below tests a small Wallet. The class name and the method names start with the word test so the loader finds them. setUp gives each test a new wallet holding 100, so spending in one test cannot change the next.
import unittest
class Wallet:
def __init__(self, balance=0):
self.balance = balance
def spend(self, amount):
if amount > self.balance:
raise ValueError("not enough money")
self.balance -= amount
class WalletTests(unittest.TestCase):
def setUp(self):
self.wallet = Wallet(100)
def test_spend_reduces_balance(self):
self.wallet.spend(30)
self.assertEqual(self.wallet.balance, 70)
def test_cannot_overspend(self):
with self.assertRaises(ValueError):
self.wallet.spend(500)
def test_wrong_on_purpose(self):
self.wallet.spend(10)
self.assertEqual(self.wallet.balance, 100)
if __name__ == "__main__":
unittest.main()02Run it and read the report
-v prints one line per test. A failing test gets a section with its name, the traceback and the assertion message 90 != 100. The last lines say how many tests ran and the verdict. The paths and the timing differ on your machine (the traceback path is trimmed here).
03The order of setUp and tearDown
For every test method: a new instance of the class is created, setUp runs, the test runs, tearDown runs. setUpClass and tearDownClass wrap the whole class once. If setUp raises an error, the test is reported as an error and tearDown is skipped, so keep setUp simple.
04Run unittest from your own code
The runnable example builds a suite, runs it with a TextTestRunner that writes to a hidden stream, then reads the result object: testsRun, failures, errors, wasSuccessful(). This is how tools and notebooks drive unittest, and it avoids the timing line that changes between runs.
A failure means an assertion was false. An error means the test crashed with another exception (a bug in the code or the test). They are counted apart.
05doctest: examples that check themselves
Write the example in the docstring exactly as it would look in the interactive shell (Topic 0.5). doctest finds every >>>, runs it, and compares what was printed. A module's doctests run with doctest.testmod(module).
Here the first two examples are real documentation: a reader sees how to call double, and the tests keep the docs honest.
def double(n):
"""Return n times two.
>>> double(4)
8
>>> double(-1)
-2
"""
return n * 206When a doctest fails
The report shows the file and line, the example, what was expected and what was actually printed. In the second runnable doctest example, the docstring says 'OK' but the function returns 'OK!'. The output also tells you how many examples were tried.
07Directives for text that changes
Output that contains memory addresses, long lines or empty lines needs help. +ELLIPSIS makes ... a wildcard, +NORMALIZE_WHITESPACE ignores spacing, and <BLANKLINE> marks an empty line. The last runnable example uses all three. If you need many of these, a regular test is probably clearer than a doctest.
08Reading and adopting a legacy suite
When you join a project with unittest tests, you do not have to rewrite them. Run them with pytest and you get better failure output and -k, -x, --lf for free. Write new tests in the pytest style beside them. When you touch an old test file anyway, convert it gradually.
Translation guide: self.assertEqual(a, b) is assert a == b. self.assertTrue(x) is assert x. with self.assertRaises(E): is with pytest.raises(E):. setUp becomes a fixture. self.assertAlmostEqual(a, b) is assert a == pytest.approx(b).
Try it yourself
- 1
Fix the failing test
In the first example, change the expected value in
test_wrong_on_purposeto90and run. Check thattests run: 4andwas successful: True. - 2
Add a setUp-dependent test
Add
test_spend_everythingthat spends 100 and checks the balance is 0. Then add a second test that spends 100 as well. Both pass, becausesetUpgives each its own wallet. Predict what would happen if the wallet were created once outside the class. - 3
Break a doctest
In the passing doctest example change
-2to-3. Run it and read the report: the expected text, the text it got, and the line number.
Code & diagrams
One test is wrong on purpose (a wallet that spent 10 cannot still have 100). Fix the expected value and run again.
Expected output
tests run: 4
failures: 1
errors: 0
FAILED: __main__.WalletTests.test_wrong_on_purpose
AssertionError: 90 != 100
was successful: Falsedoctest looks inside a module, so the code is written to mathbits.py first. `verbose=True` shows every example it tried.
Expected output
Trying:
double(4)
Expecting:
8
ok
Trying:
double(-1)
Expecting:
-2
ok
Trying:
shout("hi")
Expecting:
'HI!'
ok
Trying:
shout("ok")
Expecting:
'OK!'
ok
1 item had no tests:
mathbits
2 items passed all tests:
2 tests in mathbits.double
2 tests in mathbits.shout
4 tests in 3 items.
4 passed.
Test passed.
TestResults(failed=0, attempted=4)The second example in `shout` expects 'OK' but the function returns 'OK!'. The code hides the temporary folder name so the report looks the same on every computer.
Expected output
**********************************************************************
File "mathbits.py", line 18, in mathbits.shout
Failed example:
shout("ok")
Expected:
'OK'
Got:
'OK!'
**********************************************************************
1 item had failures:
1 of 2 in mathbits.shout
***Test Failed*** 1 failure.
TestResults(failed=1, attempted=4)Remove `# doctest: +ELLIPSIS` from the first example and run again: the changing memory address makes it fail.
Expected output
TestResults(failed=0, attempted=3)Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
A test method without the test prefix
Rename test_cannot_overspend to cannot_overspend and run the suite.
import io
import unittest
class Demo(unittest.TestCase):
def test_one(self):
self.assertEqual(1 + 1, 2)
def cannot_overspend(self): # no test prefix: never collected
self.assertEqual(1 + 1, 3)
suite = unittest.defaultTestLoader.loadTestsFromTestCase(Demo)
result = unittest.TextTestRunner(stream=io.StringIO()).run(suite)
print("tests run:", result.testsRun, "- successful:", result.wasSuccessful())Myth vs fact
Myth
unittest is obsolete and pytest has replaced it.
Fact
unittest is maintained, ships with Python and underlies a lot of existing code. pytest is more comfortable, and it runs unittest tests, so the two work together.
Myth
doctests are a replacement for a real test suite.
Fact
They test short, pure examples. They are fragile for setup, randomness and long output. Use them to keep documentation true, and use real tests for behaviour.
Myth
assertEqual(a, b) is just a longer way of writing assert a == b.
Fact
Under plain unittest a bare assert a == b fails with no detail, so assertEqual exists to print both values (and a diff for long strings, lists and dicts). Under pytest, assert rewriting gives equal detail for a plain assert, but assertEqual and friends still work.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
unittest.TestCase.subTestis the unittest answer to parametrize:with self.subTest(n=n):inside a loop reports every failing case with its parameters instead of stopping at the first. It does not produce separate test ids, which is one reason pytest's parametrize is nicer. - ▸
unittest.IsolatedAsyncioTestCase(Python 3.8+) runsasync def test_...methods andasyncSetUp/asyncTearDown, which matters for tests of asyncio code (Phase 12). PlainTestCasecannot await. - ▸
doctest examples run with the module's globals copied in, so they do not see leaks from each other, but they do share state within one docstring: a variable set in the first example is available in the second. Put one idea in each docstring and avoid long chains.
- ▸
doctest can test errors too: write the expected output as the
Traceback (most recent call last):header line, then the last line of the traceback (ValueError: ...); the lines in between are ignored.# doctest: +IGNORE_EXCEPTION_DETAILalso ignores the message text, which differs between Python versions. - ▸
self.addCleanup(function, *args)registers teardown at the moment you acquire a resource. Cleanups run in reverse order even whensetUpitself fails half-way, whichtearDowndoes not do, so preferaddCleanupfor anything acquired insetUp(a started patcher, an open file). Under pytest, aTestCasemethod cannot take fixtures as arguments and cannot useparametrize; use@pytest.mark.usefixtures("name")or anautousefixture instead. - ▸
Exit codes:
python -m unittestexits with 1 on failure, which is what CI looks at.doctest.testmod()returnsTestResults(failed, attempted); a script that wants a non-zero exit must callsys.exititself, or you can run doctests under pytest.
Remember this
- 1
unittestfollows the xUnit design that came from Java's JUnit. You write a class (Topic 8.1) that inherits fromunittest.TestCase(Topic 8.5). Each method whose name starts withtestis one test. Inside, you use methods such asself.assertEqual(a, b)instead of a bareassert. - 2
Many older projects and libraries (and Python's own test suite) are written this way, so you will meet it even if you prefer pytest.
- 3
setUpruns before every test method andtearDownruns after it (even when the test fails). Use them to give each test a fresh object, like theWallet(100)in the example. Each test method runs on its own new instance of the class, which keeps tests apart. - 4
setUpClassandtearDownClass(class methods, Topic 8.3) run once for the whole class. They are the unittest version of a wider-scope fixture (Topic 11.3). - 5
The assertion methods tell you what failed.
assertEqual(a, b)fails with90 != 100.assertTrue,assertFalse,assertIs,assertIsNone,assertInandassertAlmostEqual(for floats) cover the common cases. For errors usewith self.assertRaises(ValueError):(awithblock, Topic 7.4), orassertRaisesRegexto match the message. - 6
self.subTest(...)runs several cases in one method and reports each failing one separately.@unittest.skipand@unittest.expectedFailuremark tests that should not count. - 7
Run a suite with
python -m unittest(it discovers files namedtest*.py), add-vto list each test, or name one:python -m unittest test_wallet.WalletTests.test_cannot_overspend. A script can callunittest.main(). The exit code (Topic 11.2) is non-zero if anything failed, so CI can use it. - 8
Underneath, a loader builds a suite of tests, a runner runs them, and a result object counts passes, failures and errors. The runnable example uses these directly.
- 9
doctest searches docstrings (the text in triple quotes right under a
def, Topic 5.9) for text that looks like an interactive session (Topic 0.5). A line starting with>>>is code, and the lines under it are the expected output. It runs the code and compares the printed output text exactly. - 10
python -m doctest -v module.pyordoctest.testmod()run them, andpytest --doctest-modulesruns them inside pytest. They are perfect for short, pure examples that also serve as documentation. They are poor for anything with setup, randomness, or large output. - 11
Because doctest compares text, you sometimes need directives.
# doctest: +ELLIPSISlets...match any text (hiding a memory address, the number that says where an object sits in the computer, which changes every run).+NORMALIZE_WHITESPACEignores differences in spaces and line breaks.<BLANKLINE>stands for an empty line in expected output. - 12
Without directives, a doctest that prints a dictionary or a memory address fails for reasons that have nothing to do with correctness.
- 13
Reading a legacy unittest suite is mostly translation.
self.assertEqual(a, b)isassert a == b;setUpis a fixture;assertRaisesispytest.raises. The good news is that **pytest runsTestCaseclasses unchanged**, so you can adopt pytest as the runner for an old suite today and write new tests in pytest style next to it. You do not need to rewrite working tests.
Explain it without notes
How do you write a unittest test, and what do setUp and tearDown do?
What is the difference between a failure and an error in unittest?
How does doctest work, and where does it fall short?
How would you move a legacy unittest suite to pytest?
What do the +ELLIPSIS and +NORMALIZE_WHITESPACE directives do?
Practice
Write a TestCase for a function is_even(n) with three tests, and run it in the browser with a TextTestRunner writing to a hidden stream. Print result.testsRun and result.wasSuccessful().
Write a function initials(name) with a docstring containing two doctest examples, put it in a module file and run doctest.testmod.
Use subTest to check is_even for several numbers in one test method.
Trade-offs
- ↔
unittest: no install, more ceremony. It is always available and explicit, but needs classes and long assertion method names. pytest is shorter but is a dependency.
- ↔
doctest: documentation that cannot rot, but fragile. Examples stay correct because they run, but exact text matching makes them break on harmless formatting changes. Keep them small.
- ↔
Rewriting old tests: low value, high risk. A working test suite that still runs has value. Convert when you change the code under it, not for fashion.
Done when you can
Done when you can write a
TestCasewithsetUpand assertion methods and run it.Done when you can explain the difference between a failure and an error.
Done when you can write a docstring with
>>>examples and run it withdoctest.Done when you can use
+ELLIPSISand<BLANKLINE>in a doctest.Done when you can read a unittest-based test file and say how its pieces map to pytest.