Testing with unittest
Python’s standard library ships a test framework, a mocking library that is genuinely more powerful than
most third-party equivalents, a doctest runner, and a discovery mechanism. Everyone reaches for pytest —
which is a reasonable choice — but unittest.mock is what pytest users import anyway, and the
unittest half is what you get on a locked-down machine, in a container with no network, and in every
CPython codebase written before 2015.
Everything here was executed on CPython 3.11.15: 102 tests, all passing (5 skipped, 1 expected failure), across seven test modules. Where a feature needs 3.12+ I have labelled it.
Table of contents
- 1. Structure and fixtures
- 2. The assertion surface
- 3. Skipping and expected failures
- 4.
subTestand parametrization - 5.
unittest.mock: Mock and MagicMock - 6. Configuring behaviour:
return_valueandside_effect - 7. Asserting on calls
- 8.
patch: the seven forms - 9. Where to patch
- 10.
spec,spec_set, andautospec - 11.
mock_openand other helpers - 12. Async testing
- 13. doctest
- 14. Integration testing with the standard library
- 15. Running, discovering, selecting
- 16. Contract tests and shared suites
- 17. Gotchas found by running this
- 18. unittest vs pytest
- 19. Interview questions
1. Structure and fixtures
sequenceDiagram
participant Mod as Module
participant Cls as TestClass
participant Test as Test method
Mod->>Mod: setUpModule()
Mod->>Cls: setUpClass()
loop each test method
Cls->>Test: setUp()
Test->>Test: test_*()
Test->>Cls: tearDown()
end
Cls->>Mod: tearDownClass()
Mod->>Mod: tearDownModule()
import unittest
def setUpModule(): ... # once, before anything in this module
def tearDownModule(): ... # once, after everything
class TestStructure(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.shared = {"expensive": True} # built once for the whole class
@classmethod
def tearDownClass(cls): ...
def setUp(self):
self.per_test = [] # fresh for every test method
def tearDown(self): ...
def test_a_first(self): ...
def test_b_second(self): ...
The order, asserted rather than described:
setUpModule -> setUpClass -> (setUp -> test -> tearDown)* -> tearDownClass -> tearDownModule
def test_c_order_so_far(self):
self.assertEqual(order_log[:6],
["setUpModule", "setUpClass", "setUp", "test_a", "tearDown", "setUp"])
Tests run in alphabetical order by method name within a class, and classes run in alphabetical order
within a module. That is why the tests above are named a/b/c. Relying on that order is a smell —
each test should pass in isolation — but knowing it exists explains a lot of mysterious CI behaviour, and
--test-randomize-style shuffling does not exist here (pytest has plugins for it).
addCleanup beats tearDown
def test_addCleanup_is_lifo(self):
seen = []
self.addCleanup(seen.append, 1)
self.addCleanup(seen.append, 2) # runs FIRST: cleanups are LIFO
Three reasons to prefer it:
- It runs even if
setUpraised. IfsetUpopens two resources and the second fails,tearDownnever runs and the first leaks. Registering a cleanup immediately after each acquisition fixes that. - It is LIFO, so resources are released in the reverse of acquisition order — which is what you want for anything nested.
- It lives next to the acquisition, so you cannot forget it in a distant method.
addClassCleanup is the setUpClass equivalent.
enterContext (3.11+)
The cleanest fixture idiom in modern unittest: bind a context manager to the test’s lifetime with no
nesting and no cleanup call.
def setUp(self):
self.dir = Path(self.enterContext(tempfile.TemporaryDirectory()))
self.db = self.enterContext(sqlite3.connect(":memory:"))
self.charge = self.enterContext(patch("shop.service.charge"))
Verified: the temp directory exists during the test and is gone afterwards. enterClassContext and
enterModuleContext (3.11+) do the same at the wider scopes.
2. The assertion surface
41 assert* methods. These are the ones that earn their keep.
| Method | Use |
|---|---|
assertEqual(a, b) / assertNotEqual | the workhorse; dispatches on type for better diffs |
assertTrue(x) / assertFalse(x) | truthiness — usually the wrong choice, see below |
assertIs / assertIsNot | identity, for singletons and sentinels |
assertIsNone / assertIsNotNone | the None special case |
assertIn / assertNotIn | membership |
assertIsInstance / assertNotIsInstance | type checks |
assertRaises(Exc) | as a context manager, exposes .exception |
assertRaisesRegex(Exc, r) | message must also match |
assertWarns / assertWarnsRegex | warnings |
assertLogs(logger, level) | captures records; .output and .records |
assertNoLogs(logger, level) | 3.10+; asserts nothing was logged |
assertAlmostEqual(a, b, places=/delta=) | floats |
assertGreater / Less / GreaterEqual / LessEqual | comparisons with useful messages |
assertRegex / assertNotRegex | regex against a string |
assertCountEqual(a, b) | same elements, any order, no hashability needed |
assertSequenceEqual / ListEqual / TupleEqual | sequences, with index-level diffs |
assertDictEqual / assertSetEqual | purpose-built diffs |
assertMultiLineEqual(a, b) | unified diff for strings |
fail(msg) | unconditional failure |
Prefer specific assertions
self.assertTrue([]) # AssertionError: False is not true <- tells you nothing
self.assertEqual(len([]), 0) # AssertionError: 0 != 1 <- tells you what
assertEqual dispatches on the type of its arguments, so dicts, lists, sets and multi-line strings get
purpose-built diffs automatically. assertTrue(a == b) throws that away — it is the single most common
way to make a failing test uninformative.
Exceptions, warnings and logs as context managers
with self.assertRaises(ZeroDivisionError):
divide(1, 0)
with self.assertRaises(ValueError) as ctx: # capture it for further assertions
int("nope")
self.assertIn("invalid literal", str(ctx.exception))
with self.assertRaisesRegex(ZeroDivisionError, "division by zero"):
divide(1, 0)
with self.assertWarns(DeprecationWarning):
warnings.warn("old", DeprecationWarning)
with self.assertLogs("shop", level="INFO") as cap:
log.info("order placed %s", 7)
self.assertEqual(cap.output, ["INFO:shop:order placed 7"])
self.assertEqual(cap.records[0].levelname, "INFO")
with self.assertNoLogs("shop", level="WARNING"): # 3.10+
log.info("this is below WARNING")
assertLogs is underused and excellent: logging is observable behaviour, and “logs an error when the
payment fails” is a legitimate requirement to assert. assertNoLogs is how you assert the absence of a
warning, which is otherwise very awkward.
The msg argument and longMessage
self.assertEqual(1, 2, "orders did not match")
# AssertionError: 1 != 2 : orders did not match
With longMessage = True (the default) your message is appended to the generated diff rather than
replacing it. Verified — both strings appear. Set self.longMessage = False to replace instead, which
you almost never want.
3. Skipping and expected failures
@unittest.skip("unconditional, with a reason")
def test_skipped(self): ...
@unittest.skipIf(sys.platform == "win32", "POSIX only")
def test_posix(self): ...
@unittest.skipUnless(os.environ.get("RUN_SLOW"), "set RUN_SLOW=1 to enable")
def test_slow(self): ...
def test_runtime_skip(self):
if not os.environ.get("DATABASE_URL"):
self.skipTest("no database configured") # decided at runtime
@unittest.expectedFailure
def test_known_broken(self):
self.assertEqual(1, 2) # reported as "expected failure"; does not fail the run
Output from the real run:
test_known_broken ... expected failure
test_runtime_skip ... skipped 'no database configured'
test_skipped ... skipped 'unconditional, with a reason'
OK (skipped=4, expected failures=1)
The decorators evaluate their condition at import time; skipTest evaluates at run time. That
matters when the condition depends on something a fixture sets up.
An @expectedFailure test that passes is reported as an unexpected success and, since 3.4, fails
the run. That is the mechanism that stops a fixed bug from silently staying marked broken — a genuinely
good design detail, and something node:test’s todo does not do.
4. subTest and parametrization
unittest has no @parametrize. It has something subtler, and for a table of cases it is often better.
def test_fizzbuzz_table(self):
for n, expected in [(1, "1"), (3, "Fizz"), (5, "Buzz"), (15, "FizzBuzz"), (7, "7")]:
with self.subTest(n=n, expected=expected):
self.assertEqual(fizzbuzz(n), expected)
Without subTest the loop stops at the first failure and you fix one case per run. With it, every
failing case is reported, each labelled with the keyword arguments you passed:
FAIL: test_fizzbuzz_table (__main__.TestX.test_fizzbuzz_table) [n=5, expected='Buzz']
The keyword arguments are the whole point — always pass the loop variables so the failure identifies itself.
When you want real separate tests
subTest cases are not individually selectable, do not appear as separate entries in CI reports, and do
not respect --failfast per case. When that matters, generate methods:
def _make_test(n, expected):
def test(self):
self.assertEqual(fizzbuzz(n), expected)
test.__name__ = f"test_fizzbuzz_{n}"
test.__doc__ = f"fizzbuzz({n}) == {expected!r}"
return test
class TestGenerated(unittest.TestCase):
pass
for _n, _e in CASES:
setattr(TestGenerated, f"test_fizzbuzz_{_n}", _make_test(_n, _e))
This is what pytest.mark.parametrize does under the hood. It runs, it is selectable
(python -m unittest test_patterns.TestGenerated.test_fizzbuzz_15), and it is uglier — which is the
honest trade-off.
Property-style testing with the stdlib
No hypothesis needed for the cheap version: random inputs plus an invariant, with a fixed seed so
a failure reproduces.
def test_fizzbuzz_invariants(self):
rng = random.Random(20260820) # seeded -> deterministic
for _ in range(500):
n = rng.randint(1, 10_000)
with self.subTest(n=n):
out = fizzbuzz(n)
if n % 15 == 0: self.assertEqual(out, "FizzBuzz")
elif n % 3 == 0: self.assertEqual(out, "Fizz")
elif n % 5 == 0: self.assertEqual(out, "Buzz")
else: self.assertEqual(out, str(n))
What you lose versus hypothesis: shrinking (minimizing a failing input), a smart generator library, and
a failure database. What you keep: 90% of the value for 5 lines and no dependency. The invariant is doing
the work, not the generator.
5. unittest.mock: Mock and MagicMock
A Mock answers any attribute access by creating a child mock, and any call by returning
return_value.
m = Mock()
m.anything.at.all() # auto-creates the whole chain, records the call
m.f(1, b=2)
m.f.assert_called_once_with(1, b=2)
That auto-creation is the library’s greatest strength and its central hazard — see section 10.
The mock family
| Class | What it adds |
|---|---|
Mock | attribute auto-creation, call recording, callable |
MagicMock | Mock + configured dunder methods (__len__, __iter__, __enter__, __int__, __bool__, comparisons, …) |
NonCallableMock / NonCallableMagicMock | same but not callable — for mocking instances rather than factories |
AsyncMock | calls return awaitables; adds assert_awaited* and await_count |
PropertyMock | for @property; records the get |
sentinel | unique, self-describing placeholder objects |
ANY | matches anything in a call assertion |
call | builds call objects for assert_has_calls |
Verified difference between Mock and MagicMock:
plain, magic = Mock(), MagicMock()
len(plain) # TypeError: object of type 'Mock' has no len()
len(magic) # 0
list(magic) # []
with magic as ctx: # works: __enter__ is configured
...
int(magic) # 1
bool(magic) # True
patch() gives you a MagicMock by default, which is why patched objects usually “just work” in
with blocks and len() calls. Use plain Mock when you want a dunder to fail loudly.
seal and sentinel
m = Mock()
m.known = 1
seal(m) # stop auto-creation from here down
m.typoed_attribute # AttributeError
m(sentinel.request_id, timestamp=12345)
m.assert_called_once_with(sentinel.request_id, timestamp=ANY)
assert sentinel.request_id is sentinel.request_id # same object every time
sentinel beats "some-string" for opaque values passed through a system: it cannot accidentally equal
a real value, and its repr tells you where it came from (sentinel.request_id).
6. Configuring behaviour: return_value and side_effect
m = Mock(return_value=42)
m() # 42
m.child.return_value = "child result" # configure nested mocks
Mock(**{"find_by_sku.return_value": {"price": 10}}) # or via the constructor's kwargs form
side_effect has four modes, and knowing all four is the difference between fighting the library and
using it:
# 1. An exception class or instance -> raised on call
m = Mock(side_effect=ValueError("boom"))
# 2. An iterable -> one item per call, then StopIteration
m = Mock(side_effect=[1, 2, 3])
[m(), m(), m()] # [1, 2, 3]
m() # StopIteration
# 3. A callable -> its return value is used (and it sees the arguments)
m = Mock(side_effect=lambda a, b: a * b)
m(3, 4) # 12
# 4. A callable returning DEFAULT -> falls back to return_value
m = Mock(return_value="fallback", side_effect=lambda x: DEFAULT if x else "computed")
m(0) # 'computed'
m(1) # 'fallback'
All four verified. Mode 2 is how you script a sequence of responses (first call fails, second succeeds —
exactly what a retry test needs). Mode 3 is how you make a fake behave like a real thing: a side_effect
that reads from a dict is a fake repository, not a stub.
wraps=real_object is the fifth option: record calls while delegating to the real implementation — the
partial-mock case.
m.reset_mock() # clear call history
m.reset_mock(return_value=True, side_effect=True) # also clear the configuration
7. Asserting on calls
m.assert_not_called()
m.assert_called()
m.assert_called_once()
m.assert_called_with(1) # the LAST call only
m.assert_called_once_with(1) # exactly one call, with these args
m.assert_any_call(1) # anywhere in the history
m.assert_has_calls([call(1), call(2)]) # in order, as a subsequence
m.assert_has_calls([call(2)], any_order=True)
Inspecting directly, when an assertion helper does not fit:
m.call_count # 2
m.call_args # call('b') — the last call
m.call_args_list # [call('a', k=1), call('b')]
m.call_args_list[0].args # ('a',) — .args/.kwargs since 3.8
m.call_args_list[0].kwargs # {'k': 1}
m.method_calls # calls to children, not the mock itself
m.mock_calls # the WHOLE tree, including children and their returns
mock_calls records the full tree, which is how you assert on a chain:
m.child.grandchild(1)
m.other()
assert m.mock_calls == [call.child.grandchild(1), call.other()]
assert_called_with asserts on the last call only
This is the assertion people misread. If the mock was called three times, assert_called_with(x) checks
only the third. Use assert_any_call, assert_has_calls, or call_args_list when the order or the
history matters — and prefer assert_called_once_with when you believe there was exactly one call,
because it will catch an accidental double invocation.
8. patch: the seven forms
# 1. Context manager — scoped, explicit, my default.
with patch("shop.service.charge", return_value={"id": "rcpt"}) as charge:
...
charge.assert_called_once_with("tok", 20)
# 2. Decorator — the mock is APPENDED to the signature.
@patch("shop.service.charge", return_value={"id": "rcpt"})
def test_x(self, charge): ...
# 3. Stacked decorators — arguments arrive BOTTOM-UP.
@patch("shop.service.charge") # -> second parameter
@patch("shop.service.time") # -> first parameter (closest to the function)
def test_y(self, mock_time, mock_charge): ...
# 4. Class decorator — patches every test_* method in the class.
@patch("shop.service.charge", return_value={"id": "class_level"})
class TestClassLevelPatch(unittest.TestCase):
def test_one(self, charge): ...
def test_two(self, charge): ...
# 5. patch.object — patch an attribute of an object you already have.
with patch.object(Client, "fetch", return_value="patched"): ...
# 6. patch.dict — temporarily change a dict (env vars, registries, caches).
with patch.dict(os.environ, {"STAGE": "test"}): ...
with patch.dict(cfg, {"b": 2}, clear=True): ... # cfg == {"b": 2} inside, restored after
# 7. Manual start/stop, with guaranteed cleanup.
p = patch("shop.service.charge", return_value={"id": "manual"})
charge = p.start()
self.addCleanup(p.stop)
The stacking order in form 3 is the classic interview trip-up: decorators apply bottom-up, so the closest decorator supplies the first mock argument. Verified.
Form 7 with addCleanup is the right pattern for a patch that every test in a class needs — it belongs
in setUp, and addCleanup guarantees the unpatch even if the test errors. In 3.11+, enterContext is
tidier still:
def setUp(self):
self.charge = self.enterContext(patch("shop.service.charge", return_value={"id": "rcpt_1"}))
Other options worth knowing: new= to supply the replacement yourself (then no argument is injected),
new_callable=PropertyMock for properties, create=True to patch a name that does not exist yet (for
optional dependencies), and patch.multiple for several names at once.
with patch.object(Cfg, "region", new_callable=PropertyMock, return_value="eu-west-1") as p:
Cfg().region # 'eu-west-1'
p.assert_called_once_with() # a PropertyMock records the GET
9. Where to patch
The single most important rule in this chapter: patch where the name is looked up, not where it is defined.
# shop/service.py
from .gateway import charge # <- this binds the name `charge` in shop.service
def place(...):
receipt = charge(card, total) # resolved as shop.service.charge
patch("shop.gateway.charge") # WRONG — shop.service still holds its own reference
patch("shop.service.charge") # RIGHT
Demonstrated with both patches active at once, to make the precedence unambiguous:
with patch("shop.gateway.charge", return_value={"id": "WRONG"}):
with patch("shop.service.charge", return_value={"id": "RIGHT"}):
assert svc.place("A", 1, "t")["id"] == "RIGHT"
The mechanism: from X import name copies the object reference into the importing module’s namespace at
import time. Rebinding X.name afterwards does not touch that copy.
The corollary is that import style determines patchability:
from .gateway import charge # must patch shop.service.charge
charge(...)
from . import gateway # must patch shop.gateway.charge — the lookup happens at CALL time
gateway.charge(...)
The second style is more patchable, which is a real argument for import module over
from module import name in code you expect to test. And the best answer is neither: pass the
dependency in and patch nothing.
class OrderService:
def __init__(self, repo, charge=charge, clock=time.time): ...
Then the test just passes a different charge — no string paths, no import-order coupling, no patching
at all. Every patch("some.dotted.path") in a test suite is a small piece of coupling between the test
and the module layout: rename a module and the tests break without a single line of production code
changing. That is the argument the design chapters make, arriving here as a concrete cost.
10. spec, spec_set, and autospec
Auto-creation means a plain Mock accepts anything, including your typos. This is the failure mode
that lets a test pass while the code is broken.
m = Mock()
m.find_by_skew("typo") # silently fine — creates a new child mock and records the call
m.find_by_sku() # wrong arity, also fine
Verified: both calls succeed and appear in method_calls. The production code calling find_by_sku
would AttributeError in production while the test stays green.
Good news first: misspelled assertion methods are caught. Mock special-cases names starting with
assert or assret:
m.assert_called_onse_with(999)
# AttributeError: 'assert_called_onse_with' is not a valid assertion.
# Use a spec for the mock if 'assert_called_onse_with' is meant to be an attribute.
That protection does not extend to ordinary method names. For those you need a spec.
The three levels
class Repo:
def find_by_sku(self, sku): ...
def decrement(self, sku, qty): ...
# spec: names are checked, signatures are NOT
m = Mock(spec=Repo)
m.find_by_sku("ABC") # ok
m.find_by_skew("ABC") # AttributeError
m.find_by_sku() # ok?! spec checks names only
m.find_by_sku(1, 2, 3, 4) # also ok
# spec_set: like spec, and you cannot ADD attributes either
m = Mock(spec_set=Repo)
m.new_attribute = 1 # AttributeError
# autospec: names AND signatures, recursively
m = create_autospec(Repo, instance=True)
m.find_by_sku("ABC") # ok
m.find_by_sku() # TypeError: missing a required argument: 'sku'
m.decrement("ABC") # TypeError: missing a required argument: 'qty'
m.nope() # AttributeError
All verified. And with patch:
with patch("shop.service.charge", autospec=True) as charge:
charge.return_value = {"id": "auto"}
svc.place("ABC", 3, "tok")
charge.assert_called_once_with("tok", 15)
charge("only-one-arg") # TypeError — the real charge() takes (card, amount)
A spec’d mock also passes isinstance:
assert isinstance(Mock(spec=Repo), Repo) # __class__ is faked
The recommendation
Use autospec=True by default. It costs one keyword argument and it converts an entire class of
silent-green failures into loud errors. The specific bug it prevents — a refactor renames a method, the
production call site is updated, a mocked call site in a test is not, and the test keeps passing — is one
of the most common reasons a test suite stops being trustworthy.
Two caveats to know. create_autospec does not accept the Mock(**{"child.return_value": x})
kwargs form; configure children after construction:
repo = create_autospec(Repo, instance=True)
repo.find_by_sku.return_value = {"price": 5, "stock": 9} # not via **kwargs
And autospec inspects the object at patch time, so it cannot know about attributes created dynamically in
__init__ — for those, spec on an instance rather than the class, or set them explicitly.
11. mock_open and other helpers
m = mock_open(read_data="line1\nline2\n")
with patch("builtins.open", m):
with open("whatever.txt") as f:
assert f.read() == "line1\nline2\n"
m.assert_called_once_with("whatever.txt")
# Iteration works too
m = mock_open(read_data="a\nb\n")
with patch("builtins.open", m):
with open("f") as f:
assert list(f) == ["a\n", "b\n"]
# Asserting on writes: the handle is m()
m = mock_open()
with patch("builtins.open", m):
with open("out.txt", "w") as f:
f.write("hello "); f.write("world")
handle = m()
handle.write.assert_has_calls([call("hello "), call("world")])
assert "".join(c.args[0] for c in handle.write.call_args_list) == "hello world"
That last line is the idiom worth stealing: write is usually called many times, so join the recorded
fragments rather than asserting on any single call.
But prefer tempfile. mock_open tests that your code called open and write; a temp directory
tests that the file ends up correct — including encoding, newline translation, permissions, and the
ENOENT you get for a missing parent directory. See
section 14. Reach for mock_open when the
filesystem is genuinely not available or when you need to simulate an I/O error that is hard to provoke
for real.
12. Async testing
unittest.IsolatedAsyncioTestCase gives each test its own event loop plus async fixtures.
class TestAsyncMock(unittest.IsolatedAsyncioTestCase):
async def asyncSetUp(self):
self.repo = AsyncMock()
self.repo.find_by_sku.return_value = {"price": 100}
self.payments = AsyncMock()
self.payments.charge.return_value = {"id": "rcpt"}
async def asyncTearDown(self): ...
async def test_awaiting_an_asyncmock(self):
result = await self.svc.place("ABC", 2)
self.repo.find_by_sku.assert_awaited_once_with("ABC")
self.payments.charge.assert_awaited_once_with(200)
self.assertEqual(self.payments.charge.await_count, 1)
self.assertEqual(self.payments.charge.await_args, call(200))
AsyncMock adds an entire parallel assertion vocabulary: assert_awaited,
assert_awaited_once, assert_awaited_with, assert_awaited_once_with, assert_any_await,
assert_has_awaits, assert_not_awaited, plus await_count, await_args, await_args_list.
Called and awaited are different events, which is the distinction that catches a real bug class — creating a coroutine and forgetting to await it:
coro = self.repo.find_by_sku("X") # called
self.repo.find_by_sku.assert_called_once()
self.assertEqual(self.repo.find_by_sku.await_count, 0) # not yet awaited
await coro
self.repo.find_by_sku.assert_awaited_once()
The classic async mocking bug
bad = Mock()
await bad.find_by_sku("X")
# TypeError: object Mock can't be used in 'await' expression
A plain Mock returns a Mock, which is not awaitable. AsyncMock (or MagicMock with an
AsyncMock child) is required. create_autospec gets this right automatically — it inspects the
target and produces an AsyncMock for every async def:
class Repo:
async def find_by_sku(self, sku): ...
m = create_autospec(Repo, instance=True)
assert isinstance(m.find_by_sku, AsyncMock)
That is another point for autospec: it cannot get the sync/async mismatch wrong.
Testing concurrency and timeouts
async def test_gather_runs_concurrently(self):
results = await self.svc.place_all(["A", "B", "C"])
self.assertEqual(self.repo.find_by_sku.await_count, 3)
self.assertEqual([c.args[0] for c in self.repo.find_by_sku.await_args_list], ["A", "B", "C"])
async def test_retry_backoff_without_waiting(self):
self.repo.find_by_sku.side_effect = [RuntimeError("down"), RuntimeError("down"), {"price": 50}]
await self.svc.with_retry("ABC")
self.assertEqual(self.sleeps, [0.1, 0.2]) # injected sleep -> assertable backoff
async def test_a_real_timeout(self):
self.repo.find_by_sku = slow # sleeps 10s
with self.assertRaises(TimeoutError):
async with asyncio.timeout(0.01): # 3.11+
await self.svc.place("ABC", 1)
The side_effect list is how you script “fails twice, then succeeds”, and the injected sleep
(an AsyncMock that appends to a list) is how the backoff durations become assertable rather than
merely absent. Same argument as the Node chapter: injecting the clock beats mocking it.
Without IsolatedAsyncioTestCase you drive the loop yourself, which works but costs you the async
fixtures:
def test_via_asyncio_run(self):
self.assertEqual(asyncio.run(scenario()), "ok")
13. doctest
Doctests are executable documentation: examples in a docstring that must actually produce what they claim.
def divide(a: float, b: float) -> float:
"""Divide a by b.
>>> divide(10, 4)
2.5
>>> divide(9, 3)
3.0
>>> divide(1, 0)
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
"""
return a / b
python -m doctest shop/mathutil.py # silent on success
python -m doctest shop/mathutil.py -v # "5 tests in 3 items. 5 passed and 0 failed."
Wire them into the main suite so they cannot rot unnoticed:
# test_doctests.py
import doctest
import shop.mathutil
def load_tests(loader, tests, ignore):
"""unittest's extension hook: add doctests to this module's suite."""
tests.addTests(doctest.DocTestSuite(shop.mathutil))
return tests
Doctest: shop.mathutil.divide ... ok
Doctest: shop.mathutil.fizzbuzz ... ok
OK
load_tests is worth knowing on its own — it is the documented hook for customizing what a module
contributes to the suite, and it also handles doctest.DocFileSuite for .txt documentation files.
What doctests are good for: small pure functions where the example is the best documentation, README
examples that must stay correct, and the tutorial sections of a library. What they are bad for:
anything with non-deterministic output (dict ordering pre-3.7, addresses, timestamps, floats), anything
needing setup, and anything where the assertion is more interesting than the value. Exact-repr matching
makes them brittle; doctest.ELLIPSIS and NORMALIZE_WHITESPACE help but signal you have outgrown them.
14. Integration testing with the standard library
No containers, no fixtures library, no network. The standard library has everything.
A real database
def setUp(self):
self.db = sqlite3.connect(":memory:")
self.db.row_factory = sqlite3.Row # dict-like rows
self.db.execute("PRAGMA foreign_keys = ON") # OFF by default in sqlite!
self.db.executescript("""
CREATE TABLE items (sku TEXT PRIMARY KEY, price INTEGER NOT NULL CHECK (price > 0),
stock INTEGER NOT NULL);
CREATE TABLE orders (id INTEGER PRIMARY KEY, sku TEXT NOT NULL REFERENCES items(sku),
qty INTEGER NOT NULL);
""")
self.addCleanup(self.db.close)
def test_enforces_constraints_a_mock_would_allow(self):
with self.assertRaises(sqlite3.IntegrityError): # PRIMARY KEY
self.db.execute("INSERT INTO items VALUES ('ABC', 1, 1)")
with self.assertRaises(sqlite3.IntegrityError): # CHECK
self.db.execute("INSERT INTO items VALUES ('XYZ', -5, 1)")
with self.assertRaises(sqlite3.IntegrityError): # FOREIGN KEY
self.db.execute("INSERT INTO orders VALUES (1, 'GHOST', 1)")
def test_transaction_rollback(self):
with self.assertRaises(sqlite3.IntegrityError):
with self.db: # commits on success, rolls back on raise
self.db.execute("UPDATE items SET stock = 0 WHERE sku = 'ABC'")
self.db.execute("INSERT INTO items VALUES ('ABC', 1, 1)") # boom
self.assertEqual(self.db.execute("SELECT stock FROM items").fetchone()["stock"], 4)
def test_sql_injection_is_a_real_test_case(self):
evil = "ABC'; DROP TABLE items; --"
self.assertIsNone(self.db.execute("SELECT * FROM items WHERE sku = ?", (evil,)).fetchone())
self.assertEqual(self.db.execute("SELECT count(*) c FROM items").fetchone()["c"], 1)
PRAGMA foreign_keys = ON is the line everyone forgets: SQLite ignores foreign keys by default, so a
test suite that omits it will not catch referential-integrity bugs. And with self.db: is a transaction
context manager — commit on success, rollback on exception.
The three constraint tests are the argument for integration testing in one place: a mocked repository happily accepts a duplicate key, a negative price, and an order against a nonexistent SKU. The real engine refuses all three.
A real filesystem
def setUp(self):
self.dir = Path(self.enterContext(tempfile.TemporaryDirectory()))
def test_real_error_codes(self):
with self.assertRaises(FileNotFoundError) as ctx:
(self.dir / "nope").read_text()
self.assertEqual(ctx.exception.errno, 2)
def test_permissions_are_real(self):
p = self.dir / "locked"; p.write_text("x"); p.chmod(0o000)
self.addCleanup(p.chmod, 0o644)
if os.geteuid() != 0:
with self.assertRaises(PermissionError):
p.read_text()
else:
self.skipTest("running as root: permission bits are not enforced")
That root check is not paranoia — it is why this test skipped in my run (containers usually run as root), and it is the kind of environment-dependence that makes a test suite portable or not.
A real HTTP server
class TestRealHttpServer(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), _Handler) # port 0
cls.port = cls.server.server_address[1]
cls.thread = threading.Thread(target=cls.server.serve_forever, daemon=True)
cls.thread.start()
@classmethod
def tearDownClass(cls):
cls.server.shutdown(); cls.server.server_close(); cls.thread.join(timeout=2)
def test_bad_request_surfaces_as_HTTPError(self):
with self.assertRaises(urllib.error.HTTPError) as ctx:
self._post("/orders", {})
self.assertEqual(ctx.exception.code, 400)
self.assertEqual(json.load(ctx.exception), {"error": "qty required"})
Two details. Port 0 lets the OS pick a free port, which removes the whole class of “passes alone,
fails in parallel” failures. And urllib raises HTTPError for 4xx/5xx rather than returning a
response — HTTPError is itself a file-like object, so json.load(ctx.exception) reads the error body.
That surprises people, and a test is the right place to discover it.
15. Running, discovering, selecting
python -m unittest # discover from the current directory
python -m unittest discover -s src -p 'test_*.py' -t .
python -m unittest -v # verbose: one line per test
python -m unittest test_mocking # one module
python -m unittest test_mocking.TestSpecAndAutospec # one class
python -m unittest test_mocking.TestSpecAndAutospec.test_autospec_checks_the_signature_too
python -m unittest discover -k autospec # by substring/glob (3.7+)
python -m unittest --failfast # stop at the first failure
python -m unittest --buffer # capture stdout/stderr, show only for failures
python -m unittest --locals # show local variables in tracebacks
python -m unittest --durations 5 # slowest 5 tests (3.12+)
python -m unittest --catch # Ctrl-C reports results so far
Verified on this suite:
python -m unittest discover
Ran 102 tests in 0.598s
OK (skipped=5, expected failures=1)
python -m unittest discover -k autospec
Ran 5 tests
OK
Discovery requires the directory to be importable — a missing __init__.py in a package directory is the
usual cause of “0 tests” — and -t sets the top-level directory when your tests live somewhere other
than the project root.
--buffer deserves a mention: it swallows print output from passing tests and shows it only for
failures, which turns a noisy suite readable without deleting anyone’s debugging.
Coverage is the one thing the standard library does not do well. python -m trace exists but is
unpleasant; coverage run -m unittest is the practical answer and the only external tool this chapter
recommends.
16. Contract tests and shared suites
When several implementations must satisfy the same behaviour, write the tests once and inherit them.
class BaseContractTests:
"""NOT a TestCase — so the loader does not run the abstract version."""
def make_store(self):
raise NotImplementedError
def test_set_then_get(self):
s = self.make_store(); s.set("k", 1)
self.assertEqual(s.get("k"), 1)
def test_missing_key_returns_none(self):
self.assertIsNone(self.make_store().get("absent"))
class TestDictStore(BaseContractTests, unittest.TestCase):
def make_store(self): return DictStore()
class TestListStore(BaseContractTests, unittest.TestCase):
def make_store(self): return ListStore()
Both classes ran all three contract tests — 15 tests from that module, all passing.
The key detail is that BaseContractTests does not inherit TestCase, so discovery ignores it. If
it did, the abstract version would run and fail on NotImplementedError. The MRO puts the mixin first so
its methods are found, and unittest.TestCase second so the assertions exist.
This is the pattern for: a real implementation versus its in-memory test double (proving the fake is
faithful — the standard answer to “how do you know your fake behaves like the real thing”), multiple
backends behind one interface, and a Protocol’s behavioural requirements.
17. Gotchas found by running this
The things that actually broke while writing this chapter, plus two documented traps.
1. Misspelled assert_* methods are caught — misspelled ordinary methods are not. I expected the
first to pass silently and it did not:
AttributeError: 'assert_called_onse_with' is not a valid assertion.
Use a spec for the mock if 'assert_called_onse_with' is meant to be an attribute.
Mock special-cases names starting with assert/assret. But m.find_by_skew(1) is still silently fine.
So the protection covers the assertion vocabulary, not your domain vocabulary — which is exactly what
autospec is for.
2. create_autospec does not accept the **{"child.return_value": x} kwargs form. Mock(**{...})
does; create_autospec(**{...}) raises. Configure children after construction.
3. patch.multiple yields only the mocks it created. Pass an explicit replacement and it is not in
the yielded dict — my assertion assertIn("charge", mocks) failed against {}. Pass
charge=DEFAULT if you want patch to create the mock and hand it to you.
4. PRAGMA foreign_keys = ON. SQLite silently ignores foreign keys otherwise, so an integration test
that omits it will not catch referential-integrity bugs.
5. Permission tests do not work as root. p.chmod(0o000) then reading it succeeds when euid == 0,
which is the default in most containers. Guard with os.geteuid() != 0 and skipTest.
And the two that did not break here but cause the most real-world grief:
6. Patching the wrong module. from x import y binds y in the importing module. Patch
consumer.y, not x.y. Section 9 demonstrates it with both patches active.
7. assert_called_with checks only the last call. With multiple calls it will happily pass while an
earlier call was wrong. Use assert_called_once_with when you mean once, assert_has_calls when order
matters.
18. unittest vs pytest
unittest | pytest | |
|---|---|---|
| Install | none | a dependency (plus plugins) |
| Test style | TestCase subclasses, self.assertX | plain functions, bare assert |
| Failure output | good, type-dispatched diffs | excellent, rewritten assertions |
| Fixtures | setUp/setUpClass, addCleanup, enterContext | @fixture with scopes and dependency injection |
| Parametrization | subTest or generated methods | @pytest.mark.parametrize |
| Mocking | unittest.mock (built in) | unittest.mock, usually via pytest-mock |
| Plugins | none | a large ecosystem (-xdist, -cov, -asyncio, hypothesis) |
| Discovery | test_*.py, Test* classes, test_* methods | more configurable |
| Runs the other’s tests | no | yes — pytest runs unittest.TestCase classes |
| Async | IsolatedAsyncioTestCase | pytest-asyncio / anyio |
The honest positions:
- pytest is the better tool for most projects. Function-based tests with plain
assert, real fixtures, andparametrizeare less code and better failure output.pytest-xdistfor parallelism andpytest-covfor coverage cover the two real gaps in the stdlib. unittest.mockis what you use either way.pytest-mockis a thin fixture wrapper around it. Everything in sections 5-11 applies unchanged under pytest.unittestis worth knowing properly because: pytest runsTestCaseclasses so migrations are incremental and mixed suites are normal; the stdlib is what you have in a locked-down environment; CPython itself and much of the stdlib are tested with it; andenterContextplusaddCleanupplussubTestcover more than most people realize.
The pragmatic advice: use pytest as the runner (pytest will happily run everything in this chapter),
write new tests as functions, and know unittest well enough to read and extend the suite you inherit.
19. Interview questions
Q: Where do you patch, and why?
A: Where the name is looked up, not where it is defined. from x import y copies the reference into
the importing module at import time, so you patch consumer.y. The deeper answer: every dotted patch
path is coupling between the test and the module layout — injecting the dependency removes the need to
patch at all.
Q: Mock vs MagicMock?
A: MagicMock pre-configures the dunder protocols (__len__, __iter__, __enter__, __bool__,
comparisons), so it works in with blocks and len() calls. Plain Mock raises TypeError for those.
patch gives you a MagicMock by default.
Q: What does autospec=True buy you?
A: Names and signatures are checked, recursively, and async def members become AsyncMock. It
turns the classic silent-green failure — a renamed method that the test still calls by its old name —
into an AttributeError, and a wrong-arity call into a TypeError. One keyword argument; use it by
default.
Q: What is the difference between spec and spec_set?
A: spec restricts which attributes can be read; spec_set also restricts which can be set, so
m.typo = 1 raises. Neither checks signatures — that is autospec.
Q: Name the four modes of side_effect.
A: An exception (raised), an iterable (one value per call, then StopIteration), a callable (its
return value is used and it sees the arguments), and a callable returning DEFAULT (falls back to
return_value). The iterable form is how you script “fails twice, then succeeds”.
Q: assert_called_with vs assert_called_once_with vs assert_has_calls?
A: The first checks the last call only. The second additionally asserts there was exactly one call. The third checks an ordered subsequence of the history. Reaching for the first when a mock was called several times is a common source of tests that pass while the code is wrong.
Q: Why prefer addCleanup over tearDown?
A: It runs even when setUp fails partway, it is LIFO so nested resources unwind correctly, and it
lives next to the acquisition so it cannot be forgotten. In 3.11+, enterContext is tidier again for
anything with a context manager.
Q: What does subTest do that a loop does not?
A: Reports every failing case instead of stopping at the first, each labelled with the keyword arguments you passed. The cost is that the cases are not individually selectable — generate methods when you need that.
Q: How do you test async code?
A: IsolatedAsyncioTestCase for the loop and asyncSetUp/asyncTearDown, AsyncMock for
collaborators, and the assert_awaited* family. Note that called and awaited are separate events —
await_count == 0 with call_count == 1 catches a forgotten await.
Q: What goes wrong if you use Mock where an AsyncMock is needed?
A: TypeError: object Mock can't be used in 'await' expression. create_autospec avoids the
mistake entirely by inspecting the target and producing AsyncMock for coroutine functions.
Q: How do you test retry backoff without waiting?
A: Inject the sleep function and assert on the durations it received ([0.1, 0.2]). That is stronger
than mocking time.sleep, because it proves the code waited the right amount, and it does not change
global behaviour for every other library in the process.
Q: When is mock_open the wrong tool?
A: Almost whenever a real temp directory is available. mock_open verifies you called open and
write; tempfile verifies the file is actually correct, including encoding, newlines, permissions and
real error codes. Keep mock_open for simulating I/O errors and for environments with no writable disk.
Q: What is sentinel for?
A: Unique, self-describing placeholder objects for values that pass through a system without being
inspected. Unlike "some-string" they cannot collide with a real value, and their repr names them.
Q: How would you prove an in-memory test double behaves like the real implementation?
A: A contract test suite: a mixin of behaviour tests, inherited by one TestCase per implementation.
Both the real store and the fake must pass the same tests. That mixin must not subclass TestCase, or
discovery will run the abstract version.
Q: How do you get coverage with the standard library?
A: You mostly do not — trace exists but is unpleasant. coverage run -m unittest plus
coverage report is the practical answer and the one external tool worth adding.
Q: What does @expectedFailure do that a skip does not?
A: It still runs the test. If it fails, that is expected and the run is green; if it passes, that is an “unexpected success” and, since 3.4, the run fails. So a fixed bug cannot silently stay marked broken.
Q: Why does python -m unittest find zero tests sometimes?
A: Discovery imports packages, so a directory without __init__.py (or a top-level directory that is
not on sys.path) yields nothing. -t sets the top-level directory; -s the start directory.
Q: Are doctests real tests?
A: They are real, and they are best at exactly one thing: keeping documentation examples honest for
small pure functions. They are brittle for anything non-deterministic and awkward for anything needing
setup. Wire them into the suite with load_tests so they cannot rot.
Q: unittest or pytest?
A: pytest as the runner for new projects — plain assert, real fixtures, parametrize, and the
plugin ecosystem for parallelism and coverage. But unittest.mock is what you import under either, and
knowing unittest properly matters because pytest runs TestCase classes, so mixed and migrating suites
are the normal case.
Q: Should you assert on log output?
A: Yes, when logging is part of the contract — an audit trail, an error path with no return value, a
metric. assertLogs gives you the records; assertNoLogs asserts the absence of a warning, which is
otherwise very awkward to express.
Next: Test strategy and integration testing for the cross-language design
chapter, or Testing with node:test for the JavaScript side.
Verify it yourself
test-py/shop/init.py
test-py/shop/mathutil.py
def divide(a: float, b: float) -> float:
"""Divide a by b.
>>> divide(10, 4)
2.5
>>> divide(9, 3)
3.0
>>> divide(1, 0)
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
"""
return a / b
def fizzbuzz(n: int) -> str:
"""Classic.
>>> [fizzbuzz(i) for i in range(1, 6)]
['1', '2', 'Fizz', '4', 'Buzz']
>>> fizzbuzz(15)
'FizzBuzz'
"""
if n % 15 == 0: return "FizzBuzz"
if n % 3 == 0: return "Fizz"
if n % 5 == 0: return "Buzz"
return str(n)
test-py/shop/errors.py
class OutOfStockError(Exception):
def __init__(self, sku: str):
super().__init__(f"out of stock: {sku}")
self.sku = sku
test-py/shop/aservice.py
import asyncio
class AsyncOrderService:
def __init__(self, repo, payments, sleep=asyncio.sleep):
self.repo, self.payments, self.sleep = repo, payments, sleep
async def place(self, sku, qty):
item = await self.repo.find_by_sku(sku)
if item is None:
raise LookupError(sku)
return await self.payments.charge(item["price"] * qty)
async def place_all(self, skus):
# Concurrency, not sequence: the whole point of the async version.
return await asyncio.gather(*(self.place(s, 1) for s in skus))
async def with_retry(self, sku, attempts=3, base=0.1):
last = None
for i in range(attempts):
try:
return await self.place(sku, 1)
except Exception as e: # noqa: BLE001 - deliberate for the retry demo
last = e
if i == attempts - 1:
raise
await self.sleep(base * 2 ** i)
raise last
test-py/shop/gateway.py
"""The real payment gateway. Tests must never reach this."""
import urllib.request, json
def charge(card: str, amount: int) -> dict:
req = urllib.request.Request("https://payments.example.com/charge",
data=json.dumps({"card": card, "amount": amount}).encode())
with urllib.request.urlopen(req, timeout=5) as r: # pragma: no cover
return json.load(r)
test-py/shop/service.py
"""Order placement. Note the import style: `from ... import charge` binds the name HERE,
which is why tests must patch `shop.service.charge`, not `shop.gateway.charge`."""
import time
from .gateway import charge
from .errors import OutOfStockError
class OrderService:
def __init__(self, repo, clock=time.time, logger=None):
self.repo = repo
self.clock = clock
self.logger = logger
def place(self, sku: str, qty: int, card: str) -> dict:
if not isinstance(qty, int) or isinstance(qty, bool) or qty <= 0:
raise ValueError("qty must be a positive integer")
item = self.repo.find_by_sku(sku)
if item is None or item["stock"] < qty:
raise OutOfStockError(sku)
total = item["price"] * qty
receipt = charge(card, total)
self.repo.decrement(sku, qty)
order = {"id": receipt["id"], "sku": sku, "qty": qty,
"total": total, "placed_at": self.clock()}
if self.logger:
self.logger.info("order placed %s", order["id"])
return order
test-py/test_basics.py
import unittest
import warnings
import logging
from shop.mathutil import divide, fizzbuzz
order_log = []
def setUpModule():
order_log.append("setUpModule")
def tearDownModule():
order_log.append("tearDownModule")
class TestStructure(unittest.TestCase):
"""Fixture order: setUpModule -> setUpClass -> (setUp -> test -> tearDown)* -> tearDownClass."""
@classmethod
def setUpClass(cls):
order_log.append("setUpClass")
cls.shared = {"expensive": True} # built once for the whole class
@classmethod
def tearDownClass(cls):
order_log.append("tearDownClass")
def setUp(self):
order_log.append("setUp")
self.per_test = [] # fresh for every test
def tearDown(self):
order_log.append("tearDown")
def test_a_first(self):
order_log.append("test_a")
self.assertTrue(self.shared["expensive"])
def test_b_second(self):
order_log.append("test_b")
self.assertEqual(self.per_test, []) # not polluted by test_a
def test_c_order_so_far(self):
# Tests run in alphabetical order by method name, which is why they are named a/b/c here.
self.assertEqual(order_log[:6],
["setUpModule", "setUpClass", "setUp", "test_a", "tearDown", "setUp"])
class TestCleanup(unittest.TestCase):
"""addCleanup runs even if setUp raises — tearDown does not."""
def test_addCleanup_is_lifo(self):
seen = []
self.addCleanup(seen.append, 1)
self.addCleanup(seen.append, 2)
# cleanups run in reverse registration order after the test; verified in the next test
self.__class__._seen = seen
def test_cleanups_ran_lifo(self):
self.assertEqual(getattr(self.__class__, "_seen", None), [2, 1])
def test_enterContext_manages_a_context_manager(self):
# 3.11+: bind a context manager to the test's lifetime without nesting `with`
import tempfile, os
d = self.enterContext(tempfile.TemporaryDirectory())
self.assertTrue(os.path.isdir(d))
self.__class__._tmpdir = d
def test_the_context_was_exited(self):
import os
d = getattr(self.__class__, "_tmpdir", None)
if d:
self.assertFalse(os.path.isdir(d)) # cleaned up automatically
class TestAssertions(unittest.TestCase):
"""The assertion surface. 41 assert* methods; these are the ones that earn their keep."""
def test_equality_and_identity(self):
self.assertEqual(2 + 2, 4)
self.assertNotEqual(2, 3)
self.assertIs(None, None)
self.assertIsNot([], [])
self.assertIsNone(None)
self.assertIsNotNone(0)
def test_truthiness(self):
self.assertTrue([1])
self.assertFalse([])
# assertTrue([]) would say "False is not true" — useless. Prefer a specific assertion:
self.assertEqual(len([]), 0)
def test_membership_and_types(self):
self.assertIn("a", "abc")
self.assertNotIn("z", "abc")
self.assertIsInstance(1, int)
self.assertNotIsInstance(1, str)
def test_type_specific_assertions_give_better_diffs(self):
# assertEqual dispatches on type: dict/list/set/str get purpose-built diffs.
self.assertDictEqual({"a": 1}, {"a": 1})
self.assertListEqual([1, 2], [1, 2])
self.assertSetEqual({1, 2}, {2, 1})
self.assertTupleEqual((1,), (1,))
self.assertMultiLineEqual("a\nb", "a\nb")
self.assertCountEqual([1, 2, 2], [2, 1, 2]) # same elements, order-insensitive
self.assertSequenceEqual((1, 2), [1, 2]) # cross-type sequence comparison
def test_numeric_comparisons(self):
self.assertGreater(3, 2)
self.assertGreaterEqual(3, 3)
self.assertLess(2, 3)
self.assertLessEqual(3, 3)
self.assertAlmostEqual(0.1 + 0.2, 0.3) # 7 decimal places by default
self.assertAlmostEqual(1.0, 1.001, places=2)
self.assertAlmostEqual(100.0, 101.0, delta=2)
self.assertNotAlmostEqual(0.1, 0.2)
def test_regex(self):
self.assertRegex("order 42 placed", r"^order \d+ placed$")
self.assertNotRegex("order", r"\d")
def test_exceptions_as_context_managers(self):
with self.assertRaises(ZeroDivisionError):
divide(1, 0)
# The context manager exposes the exception for further assertions.
with self.assertRaises(ValueError) as ctx:
int("nope")
self.assertIn("invalid literal", str(ctx.exception))
with self.assertRaisesRegex(ZeroDivisionError, "division by zero"):
divide(1, 0)
def test_warnings(self):
with self.assertWarns(DeprecationWarning):
warnings.warn("old", DeprecationWarning)
with self.assertWarnsRegex(UserWarning, "careful"):
warnings.warn("be careful", UserWarning)
def test_logs(self):
log = logging.getLogger("shop")
with self.assertLogs("shop", level="INFO") as cap:
log.info("order placed %s", 7)
self.assertEqual(cap.output, ["INFO:shop:order placed 7"])
self.assertEqual(cap.records[0].levelname, "INFO")
with self.assertNoLogs("shop", level="WARNING"): # 3.10+
log.info("this is below WARNING")
def test_subTest_reports_every_failing_case(self):
cases = [(1, "1"), (3, "Fizz"), (5, "Buzz"), (15, "FizzBuzz"), (7, "7")]
for n, expected in cases:
with self.subTest(n=n, expected=expected):
self.assertEqual(fizzbuzz(n), expected)
def test_fail_and_longMessage(self):
with self.assertRaises(AssertionError) as ctx:
self.assertEqual(1, 2, "orders did not match")
# longMessage=True (the default) appends your message to the generated diff
self.assertIn("orders did not match", str(ctx.exception))
self.assertIn("1 != 2", str(ctx.exception))
class TestSkipping(unittest.TestCase):
@unittest.skip("unconditional, with a reason")
def test_skipped(self):
raise AssertionError("never runs")
@unittest.skipIf(True, "condition evaluated at import time")
def test_skipped_if(self):
raise AssertionError("never runs")
@unittest.skipUnless(False, "inverse of skipIf")
def test_skipped_unless(self):
raise AssertionError("never runs")
def test_runtime_skip(self):
import os
if not os.environ.get("DATABASE_URL"):
self.skipTest("no database configured")
raise AssertionError("never runs here")
@unittest.expectedFailure
def test_known_broken(self):
self.assertEqual(1, 2) # reported as 'expected failure', does not fail the run
if __name__ == "__main__":
unittest.main()
test-py/test_mocking.py
import unittest
from unittest.mock import (Mock, MagicMock, NonCallableMock, PropertyMock, AsyncMock,
patch, call, sentinel, ANY, create_autospec, mock_open, seal)
from shop.service import OrderService
from shop.errors import OutOfStockError
import shop.gateway
class TestMockBasics(unittest.TestCase):
def test_a_mock_answers_any_attribute_and_call(self):
m = Mock()
m.anything.at.all() # auto-creates the whole chain
self.assertIsInstance(m.whatever, Mock)
m.f(1, b=2)
m.f.assert_called_once_with(1, b=2)
def test_return_value(self):
m = Mock(return_value=42)
self.assertEqual(m(), 42)
m.child.return_value = "child result"
self.assertEqual(m.child(), "child result")
def test_side_effect_exception(self):
m = Mock(side_effect=ValueError("boom"))
with self.assertRaisesRegex(ValueError, "boom"):
m()
def test_side_effect_iterable_yields_one_per_call(self):
m = Mock(side_effect=[1, 2, 3])
self.assertEqual([m(), m(), m()], [1, 2, 3])
with self.assertRaises(StopIteration):
m()
def test_side_effect_callable_computes_the_answer(self):
m = Mock(side_effect=lambda a, b: a * b)
self.assertEqual(m(3, 4), 12)
def test_side_effect_with_DEFAULT_falls_back_to_return_value(self):
from unittest.mock import DEFAULT
m = Mock(return_value="fallback", side_effect=lambda x: DEFAULT if x else "computed")
self.assertEqual(m(0), "computed")
self.assertEqual(m(1), "fallback")
def test_mock_vs_magicmock(self):
plain, magic = Mock(), MagicMock()
with self.assertRaises(TypeError):
len(plain) # Mock does not implement dunder protocols
self.assertEqual(len(magic), 0) # MagicMock configures __len__, __iter__, __enter__, ...
self.assertEqual(list(magic), [])
with magic as ctx:
self.assertIsInstance(ctx, MagicMock)
self.assertEqual(int(magic), 1)
self.assertTrue(bool(magic))
def test_noncallable_and_seal(self):
obj = NonCallableMock()
with self.assertRaises(TypeError):
obj()
m = Mock()
m.known = 1
seal(m) # no more auto-creation: typos become AttributeError
with self.assertRaises(AttributeError):
m.typoed_attribute
def test_sentinel_and_ANY(self):
m = Mock()
m(sentinel.request_id, timestamp=12345)
# sentinel gives unique, self-describing, comparable placeholders
m.assert_called_once_with(sentinel.request_id, timestamp=ANY)
self.assertIs(sentinel.request_id, sentinel.request_id)
class TestCallAssertions(unittest.TestCase):
def setUp(self):
self.m = Mock()
def test_the_full_assertion_vocabulary(self):
self.m.assert_not_called()
self.m(1)
self.m.assert_called()
self.m.assert_called_once()
self.m.assert_called_with(1)
self.m.assert_called_once_with(1)
self.m(2)
with self.assertRaises(AssertionError):
self.m.assert_called_once() # called twice now
self.m.assert_called_with(2) # asserts on the LAST call only
self.m.assert_any_call(1) # anywhere in the history
self.m.assert_has_calls([call(1), call(2)]) # in order, subsequence allowed
self.m.assert_has_calls([call(2)], any_order=True)
def test_inspecting_calls_directly(self):
self.m("a", k=1)
self.m("b")
self.assertEqual(self.m.call_count, 2)
self.assertEqual(self.m.call_args, call("b"))
self.assertEqual(self.m.call_args_list, [call("a", k=1), call("b")])
# call_args is a tuple-like: (args, kwargs) — and has .args/.kwargs since 3.8
self.assertEqual(self.m.call_args_list[0].args, ("a",))
self.assertEqual(self.m.call_args_list[0].kwargs, {"k": 1})
def test_mock_calls_records_the_whole_tree(self):
self.m.child.grandchild(1)
self.m.other()
self.assertEqual(self.m.mock_calls,
[call.child.grandchild(1), call.other()])
def test_reset_mock(self):
self.m(1)
self.m.reset_mock()
self.m.assert_not_called()
self.m.return_value = 7
self.m.reset_mock(return_value=True) # opt in to clearing configuration too
self.assertIsInstance(self.m(), Mock)
def test_misspelled_assert_methods_ARE_caught(self):
"""Modern Mock special-cases names starting with 'assert'/'assret' and raises."""
self.m(1)
with self.assertRaises(AttributeError) as ctx:
self.m.assert_called_onse_with(999) # note: 'onse'
self.assertIn("is not a valid assertion", str(ctx.exception))
def test_but_misspelled_ORDINARY_methods_are_not(self):
"""This is the bug spec/autospec exists to prevent: a typo'd production method
name silently becomes a new auto-created child mock, and the test still passes."""
self.m.find_by_sku(1)
self.m.find_by_skew(1) # typo: silently fine
self.assertEqual([c[0] for c in self.m.method_calls], ["find_by_sku", "find_by_skew"])
class TestPatching(unittest.TestCase):
"""Rule: patch where the name is LOOKED UP, not where it is defined."""
def test_patch_as_a_context_manager(self):
with patch("shop.service.charge", return_value={"id": "rcpt_ctx"}) as charge:
svc = OrderService(repo=Mock(**{"find_by_sku.return_value": {"price": 10, "stock": 5}}),
clock=lambda: 0)
order = svc.place("ABC", 2, "tok")
self.assertEqual(order["id"], "rcpt_ctx")
charge.assert_called_once_with("tok", 20)
@patch("shop.service.charge", return_value={"id": "rcpt_dec"})
def test_patch_as_a_decorator(self, charge):
# Decorator arguments arrive BOTTOM-UP and are appended to the signature.
svc = OrderService(repo=Mock(**{"find_by_sku.return_value": {"price": 10, "stock": 5}}),
clock=lambda: 0)
self.assertEqual(svc.place("ABC", 1, "tok")["id"], "rcpt_dec")
charge.assert_called_once_with("tok", 10)
@patch("shop.service.charge")
@patch("shop.service.time")
def test_stacked_patches_are_bottom_up(self, mock_time, mock_charge):
# `time` is the closest decorator -> the first parameter.
self.assertTrue(hasattr(mock_time, "time"))
self.assertTrue(hasattr(mock_charge, "assert_called"))
def test_patching_the_wrong_place_does_nothing(self):
"""shop.service did `from .gateway import charge`, so it has its OWN binding."""
with patch("shop.gateway.charge", return_value={"id": "WRONG"}):
with patch("shop.service.charge", return_value={"id": "RIGHT"}):
svc = OrderService(repo=Mock(**{"find_by_sku.return_value": {"price": 1, "stock": 1}}),
clock=lambda: 0)
self.assertEqual(svc.place("A", 1, "t")["id"], "RIGHT")
# Patching only shop.gateway.charge would leave the real function in place.
def test_patch_object(self):
class Client:
def fetch(self): return "real"
c = Client()
with patch.object(Client, "fetch", return_value="patched"):
self.assertEqual(c.fetch(), "patched")
self.assertEqual(c.fetch(), "real")
def test_patch_dict(self):
import os
with patch.dict(os.environ, {"STAGE": "test"}, clear=False):
self.assertEqual(os.environ["STAGE"], "test")
self.assertNotIn("STAGE", os.environ)
cfg = {"a": 1}
with patch.dict(cfg, {"b": 2}, clear=True):
self.assertEqual(cfg, {"b": 2})
self.assertEqual(cfg, {"a": 1})
def test_patch_multiple(self):
from unittest.mock import DEFAULT
# Explicitly-supplied replacements are NOT yielded; only DEFAULT ones are.
with patch.multiple("shop.service", charge=Mock(return_value={"id": "m"})) as mocks:
svc = OrderService(repo=Mock(**{"find_by_sku.return_value": {"price": 1, "stock": 1}}),
clock=lambda: 0)
self.assertEqual(svc.place("A", 1, "t")["id"], "m")
self.assertEqual(mocks, {}) # nothing yielded: we supplied the mock
with patch.multiple("shop.service", charge=DEFAULT) as mocks:
self.assertIn("charge", mocks) # DEFAULT -> patch creates and yields it
mocks["charge"].return_value = {"id": "d"}
svc = OrderService(repo=Mock(**{"find_by_sku.return_value": {"price": 1, "stock": 1}}),
clock=lambda: 0)
self.assertEqual(svc.place("A", 1, "t")["id"], "d")
def test_manual_start_stop_with_addCleanup(self):
p = patch("shop.service.charge", return_value={"id": "manual"})
charge = p.start()
self.addCleanup(p.stop) # guaranteed cleanup, even on failure
svc = OrderService(repo=Mock(**{"find_by_sku.return_value": {"price": 1, "stock": 1}}),
clock=lambda: 0)
self.assertEqual(svc.place("A", 1, "t")["id"], "manual")
charge.assert_called_once()
def test_new_callable_PropertyMock(self):
class Cfg:
@property
def region(self): return "us-east-1"
with patch.object(Cfg, "region", new_callable=PropertyMock, return_value="eu-west-1") as p:
self.assertEqual(Cfg().region, "eu-west-1")
p.assert_called_once_with() # a property mock records the GET
@patch("shop.service.charge", return_value={"id": "class_level"})
class TestClassLevelPatch(unittest.TestCase):
"""Decorating the class patches every test_* method, appending the mock argument."""
def test_one(self, charge):
self.assertEqual(charge.return_value["id"], "class_level")
def test_two(self, charge):
self.assertEqual(charge.return_value["id"], "class_level")
class TestSpecAndAutospec(unittest.TestCase):
"""spec/autospec turn a silent-pass typo into a loud AttributeError or TypeError."""
class Repo:
def find_by_sku(self, sku): ...
def decrement(self, sku, qty): ...
def test_plain_mock_accepts_anything(self):
m = Mock()
m.find_by_skew("typo") # silently fine — the bug this causes is real
m.find_by_sku() # wrong arity, also fine
self.assertEqual(m.method_calls[0][0], "find_by_skew")
def test_spec_catches_the_attribute_typo(self):
m = Mock(spec=self.Repo)
m.find_by_sku("ABC") # allowed
with self.assertRaises(AttributeError):
m.find_by_skew("ABC") # not on the spec
def test_spec_does_not_check_the_signature(self):
m = Mock(spec=self.Repo)
m.find_by_sku() # spec checks NAMES only, not arity
m.find_by_sku(1, 2, 3, 4)
def test_autospec_checks_the_signature_too(self):
m = create_autospec(self.Repo, instance=True)
m.find_by_sku("ABC")
with self.assertRaises(TypeError):
m.find_by_sku() # missing required argument
with self.assertRaises(TypeError):
m.decrement("ABC") # missing qty
with self.assertRaises(AttributeError):
m.nope()
def test_autospec_via_patch(self):
with patch("shop.service.charge", autospec=True) as charge:
charge.return_value = {"id": "auto"}
repo = create_autospec(self.Repo, instance=True)
# NOTE: create_autospec does NOT accept the "child.return_value" kwargs form that
# Mock(**{...}) does. Configure the child after construction.
repo.find_by_sku.return_value = {"price": 5, "stock": 9}
svc = OrderService(repo=repo, clock=lambda: 0)
svc.place("ABC", 3, "tok")
charge.assert_called_once_with("tok", 15)
with self.assertRaises(TypeError):
charge("only-one-arg") # the real charge() takes (card, amount)
def test_spec_set_forbids_new_attributes(self):
m = Mock(spec_set=self.Repo)
with self.assertRaises(AttributeError):
m.new_attribute = 1 # spec allows this; spec_set does not
def test_isinstance_passes_with_a_spec(self):
m = Mock(spec=self.Repo)
self.assertIsInstance(m, self.Repo) # __class__ is faked, so isinstance checks pass
class TestMockOpen(unittest.TestCase):
def test_read(self):
m = mock_open(read_data="line1\nline2\n")
with patch("builtins.open", m):
with open("whatever.txt") as f:
self.assertEqual(f.read(), "line1\nline2\n")
m.assert_called_once_with("whatever.txt")
def test_iteration_needs_readlines_or_3_6_plus(self):
m = mock_open(read_data="a\nb\n")
with patch("builtins.open", m):
with open("f") as f:
self.assertEqual(list(f), ["a\n", "b\n"])
def test_write_assertions(self):
m = mock_open()
with patch("builtins.open", m):
with open("out.txt", "w") as f:
f.write("hello ")
f.write("world")
handle = m()
handle.write.assert_has_calls([call("hello "), call("world")])
self.assertEqual("".join(c.args[0] for c in handle.write.call_args_list), "hello world")
class TestServiceBehaviour(unittest.TestCase):
"""What the tests are actually FOR: the branch matrix, including the negatives."""
class Repo:
def find_by_sku(self, sku): ...
def decrement(self, sku, qty): ...
def setUp(self):
self.repo = create_autospec(self.Repo, instance=True)
self.repo.find_by_sku.return_value = {"price": 250, "stock": 4}
self.logger = Mock()
self.patcher = patch("shop.service.charge", return_value={"id": "rcpt_1"})
self.charge = self.patcher.start()
self.addCleanup(self.patcher.stop)
self.svc = OrderService(repo=self.repo, clock=lambda: 1_700_000_000, logger=self.logger)
def test_happy_path(self):
order = self.svc.place("ABC", 2, "tok_x")
self.assertEqual(order, {"id": "rcpt_1", "sku": "ABC", "qty": 2,
"total": 500, "placed_at": 1_700_000_000})
self.charge.assert_called_once_with("tok_x", 500)
self.repo.decrement.assert_called_once_with("ABC", 2)
self.logger.info.assert_called_once_with("order placed %s", "rcpt_1")
def test_missing_item_does_not_charge(self):
self.repo.find_by_sku.return_value = None
with self.assertRaises(OutOfStockError) as ctx:
self.svc.place("NOPE", 1, "tok")
self.assertEqual(ctx.exception.sku, "NOPE")
self.charge.assert_not_called()
self.repo.decrement.assert_not_called()
def test_insufficient_stock_does_not_charge(self):
self.repo.find_by_sku.return_value = {"price": 10, "stock": 1}
with self.assertRaises(OutOfStockError):
self.svc.place("ABC", 5, "tok")
self.charge.assert_not_called()
def test_payment_failure_does_not_decrement(self):
self.charge.side_effect = RuntimeError("card declined")
with self.assertRaisesRegex(RuntimeError, "card declined"):
self.svc.place("ABC", 1, "tok")
self.repo.decrement.assert_not_called()
def test_validation_runs_before_any_dependency(self):
for bad in (0, -1, 2.5, True, "3"):
with self.subTest(qty=bad):
with self.assertRaises(ValueError):
self.svc.place("ABC", bad, "tok")
self.repo.find_by_sku.assert_not_called()
self.charge.assert_not_called()
if __name__ == "__main__":
unittest.main()
test-py/test_async.py
import asyncio
import unittest
from unittest.mock import AsyncMock, Mock, patch, call, create_autospec
from shop.aservice import AsyncOrderService
class TestAsyncMock(unittest.IsolatedAsyncioTestCase):
"""IsolatedAsyncioTestCase gives every test its own event loop, plus async fixtures."""
async def asyncSetUp(self):
self.repo = AsyncMock()
self.repo.find_by_sku.return_value = {"price": 100}
self.payments = AsyncMock()
self.payments.charge.return_value = {"id": "rcpt"}
self.sleeps = []
self.svc = AsyncOrderService(self.repo, self.payments,
sleep=AsyncMock(side_effect=lambda d: self.sleeps.append(d)))
async def asyncTearDown(self):
self.assertTrue(True) # runs after every async test
async def test_awaiting_an_asyncmock(self):
result = await self.svc.place("ABC", 2)
self.assertEqual(result, {"id": "rcpt"})
# Await-specific assertions, which a plain Mock does not have:
self.repo.find_by_sku.assert_awaited_once_with("ABC")
self.payments.charge.assert_awaited_once_with(200)
self.assertEqual(self.payments.charge.await_count, 1)
self.assertEqual(self.payments.charge.await_args, call(200))
async def test_await_vs_call_assertions(self):
coro = self.repo.find_by_sku("X") # CALLED but not yet awaited
self.repo.find_by_sku.assert_called_once()
self.assertEqual(self.repo.find_by_sku.await_count, 0)
await coro
self.repo.find_by_sku.assert_awaited_once()
async def test_side_effect_sequence_across_awaits(self):
self.repo.find_by_sku.side_effect = [{"price": 1}, {"price": 2}]
self.assertEqual(await self.svc.place("A", 1), {"id": "rcpt"})
self.payments.charge.assert_awaited_with(1)
await self.svc.place("B", 1)
self.payments.charge.assert_awaited_with(2)
async def test_exception_from_an_async_dependency(self):
self.repo.find_by_sku.return_value = None
with self.assertRaises(LookupError):
await self.svc.place("NOPE", 1)
self.payments.charge.assert_not_awaited()
async def test_gather_runs_concurrently(self):
results = await self.svc.place_all(["A", "B", "C"])
self.assertEqual(len(results), 3)
self.assertEqual(self.repo.find_by_sku.await_count, 3)
self.assertEqual([c.args[0] for c in self.repo.find_by_sku.await_args_list],
["A", "B", "C"])
async def test_retry_backoff_without_waiting(self):
self.repo.find_by_sku.side_effect = [RuntimeError("down"), RuntimeError("down"),
{"price": 50}]
result = await self.svc.with_retry("ABC")
self.assertEqual(result, {"id": "rcpt"})
self.assertEqual(self.sleeps, [0.1, 0.2]) # injected sleep -> assertable backoff
async def test_a_real_timeout(self):
async def slow(_):
await asyncio.sleep(10)
self.repo.find_by_sku = slow
with self.assertRaises(TimeoutError):
async with asyncio.timeout(0.01):
await self.svc.place("ABC", 1)
async def test_autospec_on_an_async_def_produces_an_AsyncMock(self):
class Repo:
async def find_by_sku(self, sku): ...
m = create_autospec(Repo, instance=True)
self.assertIsInstance(m.find_by_sku, AsyncMock) # autospec knows it is a coroutine function
m.find_by_sku.return_value = {"price": 7}
self.assertEqual(await m.find_by_sku("X"), {"price": 7})
async def test_a_plain_Mock_returned_where_a_coroutine_was_expected(self):
"""The classic async mocking bug: Mock() returns a Mock, not an awaitable."""
bad = Mock()
with self.assertRaises(TypeError):
await bad.find_by_sku("X") # object Mock can't be used in 'await' expression
class TestAsyncioRunStyle(unittest.TestCase):
"""Without IsolatedAsyncioTestCase you drive the loop yourself. Works, but you own
the loop lifecycle and lose asyncSetUp/asyncTearDown."""
def test_via_asyncio_run(self):
async def scenario():
repo = AsyncMock(); repo.find_by_sku.return_value = {"price": 3}
pay = AsyncMock(); pay.charge.return_value = "ok"
return await AsyncOrderService(repo, pay).place("A", 2)
self.assertEqual(asyncio.run(scenario()), "ok")
if __name__ == "__main__":
unittest.main()
test-py/test_integration.py
import http.server
import json
import os
import sqlite3
import tempfile
import threading
import unittest
import urllib.error
import urllib.request
from pathlib import Path
class TestSqlite(unittest.TestCase):
"""A real database with no dependency, no container, no cleanup."""
def setUp(self):
self.db = sqlite3.connect(":memory:")
self.db.row_factory = sqlite3.Row # dict-like rows
self.db.execute("PRAGMA foreign_keys = ON") # off by default in sqlite!
self.db.executescript("""
CREATE TABLE items (sku TEXT PRIMARY KEY, price INTEGER NOT NULL CHECK (price > 0),
stock INTEGER NOT NULL);
CREATE TABLE orders (id INTEGER PRIMARY KEY, sku TEXT NOT NULL REFERENCES items(sku),
qty INTEGER NOT NULL);
""")
self.db.execute("INSERT INTO items VALUES ('ABC', 250, 4)")
self.db.commit()
self.addCleanup(self.db.close)
def test_reads_real_rows(self):
row = self.db.execute("SELECT * FROM items WHERE sku = ?", ("ABC",)).fetchone()
self.assertEqual(dict(row), {"sku": "ABC", "price": 250, "stock": 4})
def test_enforces_constraints_a_mock_would_allow(self):
with self.assertRaises(sqlite3.IntegrityError): # PRIMARY KEY
self.db.execute("INSERT INTO items VALUES ('ABC', 1, 1)")
with self.assertRaises(sqlite3.IntegrityError): # CHECK
self.db.execute("INSERT INTO items VALUES ('XYZ', -5, 1)")
with self.assertRaises(sqlite3.IntegrityError): # FOREIGN KEY
self.db.execute("INSERT INTO orders VALUES (1, 'GHOST', 1)")
def test_transaction_rollback(self):
with self.assertRaises(sqlite3.IntegrityError):
with self.db: # commits on success, rolls back on raise
self.db.execute("UPDATE items SET stock = 0 WHERE sku = 'ABC'")
self.db.execute("INSERT INTO items VALUES ('ABC', 1, 1)") # boom
self.assertEqual(self.db.execute("SELECT stock FROM items").fetchone()["stock"], 4)
def test_sql_injection_is_a_real_test_case(self):
evil = "ABC'; DROP TABLE items; --"
row = self.db.execute("SELECT * FROM items WHERE sku = ?", (evil,)).fetchone()
self.assertIsNone(row)
self.assertEqual(self.db.execute("SELECT count(*) c FROM items").fetchone()["c"], 1)
class TestTempFilesystem(unittest.TestCase):
def setUp(self):
self.dir = Path(self.enterContext(tempfile.TemporaryDirectory())) # 3.11+
def test_round_trip(self):
p = self.dir / "config.json"
p.write_text(json.dumps({"stage": "test"}))
self.assertEqual(json.loads(p.read_text()), {"stage": "test"})
def test_real_error_codes(self):
with self.assertRaises(FileNotFoundError) as ctx:
(self.dir / "nope").read_text()
self.assertEqual(ctx.exception.errno, 2)
def test_permissions_are_real(self):
p = self.dir / "locked"
p.write_text("x")
p.chmod(0o000)
self.addCleanup(p.chmod, 0o644)
if os.geteuid() != 0: # root ignores permission bits
with self.assertRaises(PermissionError):
p.read_text()
else:
self.skipTest("running as root: permission bits are not enforced")
def test_isolation(self):
self.assertEqual(sorted(os.listdir(self.dir)), [])
class _Handler(http.server.BaseHTTPRequestHandler):
hits = []
def log_message(self, *a): # keep the test output clean
pass
def do_POST(self):
_Handler.hits.append(("POST", self.path))
length = int(self.headers.get("content-length", 0))
body = json.loads(self.rfile.read(length) or b"{}")
if "qty" not in body:
payload = json.dumps({"error": "qty required"}).encode()
self.send_response(400); self.send_header("content-type", "application/json")
self.send_header("content-length", str(len(payload))); self.end_headers()
self.wfile.write(payload); return
payload = json.dumps({"id": 1, "qty": body["qty"]}).encode()
self.send_response(201); self.send_header("content-type", "application/json")
self.send_header("location", "/orders/1")
self.send_header("content-length", str(len(payload))); self.end_headers()
self.wfile.write(payload)
class TestRealHttpServer(unittest.TestCase):
@classmethod
def setUpClass(cls):
_Handler.hits = []
cls.server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), _Handler) # port 0!
cls.port = cls.server.server_address[1]
cls.thread = threading.Thread(target=cls.server.serve_forever, daemon=True)
cls.thread.start()
@classmethod
def tearDownClass(cls):
cls.server.shutdown(); cls.server.server_close(); cls.thread.join(timeout=2)
def _post(self, path, payload):
req = urllib.request.Request(f"http://127.0.0.1:{self.port}{path}",
data=json.dumps(payload).encode(),
headers={"content-type": "application/json"})
return urllib.request.urlopen(req, timeout=2)
def test_created(self):
with self._post("/orders", {"qty": 3}) as res:
self.assertEqual(res.status, 201)
self.assertEqual(res.headers["location"], "/orders/1")
self.assertEqual(json.load(res), {"id": 1, "qty": 3})
def test_bad_request_surfaces_as_HTTPError(self):
with self.assertRaises(urllib.error.HTTPError) as ctx:
self._post("/orders", {})
self.assertEqual(ctx.exception.code, 400)
self.assertEqual(json.load(ctx.exception), {"error": "qty required"})
def test_the_server_saw_what_we_think(self):
self.assertIn(("POST", "/orders"), _Handler.hits)
if __name__ == "__main__":
unittest.main()
test-py/test_patterns.py
"""Parametrization and suite-building patterns unittest does not give you directly."""
import unittest
from shop.mathutil import fizzbuzz
CASES = [(1, "1"), (3, "Fizz"), (5, "Buzz"), (15, "FizzBuzz"), (7, "7"), (9, "Fizz"), (10, "Buzz")]
class TestSubTestParametrization(unittest.TestCase):
"""The built-in answer: one test, N subTests. All failures are reported, not just the first."""
def test_fizzbuzz_table(self):
for n, expected in CASES:
with self.subTest(n=n):
self.assertEqual(fizzbuzz(n), expected)
def _make_test(n, expected):
def test(self):
self.assertEqual(fizzbuzz(n), expected)
test.__name__ = f"test_fizzbuzz_{n}"
test.__doc__ = f"fizzbuzz({n}) == {expected!r}"
return test
class TestGeneratedMethods(unittest.TestCase):
"""When you want each case to be its own selectable test (for --failfast, -k, CI reporting),
generate the methods. This is what pytest.mark.parametrize does under the hood."""
pass
for _n, _e in CASES:
setattr(TestGeneratedMethods, f"test_fizzbuzz_{_n}", _make_test(_n, _e))
class BaseContractTests:
"""Shared contract tests, inherited by each implementation. NOTE: not a TestCase itself,
so the loader does not run the abstract version."""
def make_store(self):
raise NotImplementedError
def test_set_then_get(self):
s = self.make_store()
s.set("k", 1)
self.assertEqual(s.get("k"), 1)
def test_missing_key_returns_none(self):
self.assertIsNone(self.make_store().get("absent"))
def test_overwrite(self):
s = self.make_store()
s.set("k", 1); s.set("k", 2)
self.assertEqual(s.get("k"), 2)
class DictStore:
def __init__(self): self._d = {}
def set(self, k, v): self._d[k] = v
def get(self, k): return self._d.get(k)
class ListStore:
def __init__(self): self._l = []
def set(self, k, v):
self._l = [(kk, vv) for kk, vv in self._l if kk != k] + [(k, v)]
def get(self, k): return next((v for kk, v in self._l if kk == k), None)
class TestDictStore(BaseContractTests, unittest.TestCase):
def make_store(self): return DictStore()
class TestListStore(BaseContractTests, unittest.TestCase):
def make_store(self): return ListStore()
class TestPropertyStyleInvariants(unittest.TestCase):
"""Poor man's property testing with the stdlib: random inputs plus an invariant.
Seed it so a failure is reproducible."""
def test_fizzbuzz_invariants(self):
import random
rng = random.Random(20260820) # fixed seed -> deterministic failures
for _ in range(500):
n = rng.randint(1, 10_000)
with self.subTest(n=n):
out = fizzbuzz(n)
if n % 15 == 0: self.assertEqual(out, "FizzBuzz")
elif n % 3 == 0: self.assertEqual(out, "Fizz")
elif n % 5 == 0: self.assertEqual(out, "Buzz")
else: self.assertEqual(out, str(n))
if __name__ == "__main__":
unittest.main()
test-py/test_doctests.py
"""Wire doctests into the unittest suite so `python -m unittest` runs them too."""
import doctest
import shop.mathutil
def load_tests(loader, tests, ignore):
"""unittest's extension hook: add doctests to the suite for this module."""
tests.addTests(doctest.DocTestSuite(shop.mathutil))
return tests