Python · 08/10 6 min
Reading Python test failures
unittest and pytest side by side, tracebacks bottom-up, assertRaises, approx and mocking time.
Read tracebacks from the bottom
Traceback (most recent call last):
File "tests/test_ledger.py", line 31, in test_transfer
transfer(a, b, Decimal("5"))
File "bank/ledger.py", line 48, in transfer
dst.deposit(amount)
AttributeError: 'NoneType' object has no attribute 'deposit'
The last line is the error; the frame just above it is where it happened. Here dst is None, and transfer withdrew from a before finding that out.
unittest (what the labs use)
import unittest
class LedgerTests(unittest.TestCase):
def setUp(self): # runs before EACH test
self.acct = Account()
def test_deposit(self):
self.acct.deposit(Decimal("10"))
self.assertEqual(self.acct.balance, Decimal("10"))
def test_negative(self):
with self.assertRaises(ValueError):
self.acct.deposit(Decimal("-1"))
| unittest | pytest |
|---|---|
self.assertEqual(a, b) |
assert a == b |
self.assertTrue(x) |
assert x |
self.assertIn(x, xs) |
assert x in xs |
self.assertAlmostEqual(a, b) |
assert a == pytest.approx(b) |
with self.assertRaises(E): |
with pytest.raises(E, match="msg"): |
setUp / tearDown |
fixtures |
Assertion messages
AssertionError: Decimal('9.99') != Decimal('10.00'): the first value is what your code produced (actual), the second is the expected one, in the order the test wrote them. Read both before guessing.
Controlling time
from unittest import mock
with mock.patch("bucket.time.monotonic", return_value=100.0):
...
Patch the name where it's used (bucket.time.monotonic), not where it's defined. Better still, design classes to take a clock argument so tests pass a fake.
Rules
- Don't edit tests to pass them.
- Tests must not depend on order: shared module-level state (a global list, a class attribute) makes the second test fail only when run after the first.
- Re-run the whole file after each fix.
Practise it now
Next in Python
Scope, closures and function arguments