unittest is already there
A method whose name starts with test_ is a test; everything else on the class is a helper. setUp runs before each one and tearDown after, so every test starts from the same state — and the order they run in is alphabetical, which you should never depend on.
import unittest
def total(prices, tax=0.2):
return round(sum(prices) * (1 + tax), 2)
class TestTotal(unittest.TestCase):
def test_adds_tax(self):
self.assertEqual(total([100]), 120.0)
def test_empty_basket(self):
self.assertEqual(total([]), 0.0)
def test_negative_price_is_refused(self):
with self.assertRaises(ValueError):
total([-1])
if __name__ == "__main__":
unittest.main()
The assertions worth knowing
assertTrue is the one to avoid: when it fails it can only tell you that something was false. assertEqual tells you what you got and what you expected, which is the difference between a two-minute fix and a debugging session.
| Method | Use |
|---|---|
assertEqual(a, b) | The default. The failure message shows both values |
assertAlmostEqual(a, b) | Floats — 0.1 + 0.2 is not 0.3 |
assertRaises(Err) | As a context manager, so you can inspect the exception |
assertIn(x, seq) / assertIsNone(x) | Clearer failures than assertTrue(x in seq) |
assertCountEqual(a, b) | Same items, any order |
subTest(...) | A loop of cases where every failure is reported, not just the first |
The same tests, pytest-shaped
pytest is not in the standard library, and it is what almost everyone uses: plain functions, plain assert, and a rewritten assertion that still shows both sides. parametrize turns a table of cases into separate tests, each named and each reported on its own.
The same idea appears in both: a fixture (or setUp) builds the world, the test acts, and the assertion checks one thing.
def test_adds_tax():
assert total([100]) == 120.0
def test_negative_price_is_refused():
with pytest.raises(ValueError, match="negative"):
total([-1])
@pytest.mark.parametrize("prices,expected", [
([], 0.0),
([100], 120.0),
([50, 50], 120.0),
])
def test_totals(prices, expected):
assert total(prices) == expected
@pytest.fixture
def basket():
return [10, 20] # anything after a yield is teardown
What to assert
And the name is part of the test: test_refuses_negative_price tells you what broke from the report alone. test_total_2 does not.
- Behaviour, not implementation. Assert what the function returns, not which private helper it called — or the test breaks on every refactor and finds no bugs.
- The edges. Empty, one, many; zero and negative; the first and the last; the boundary the condition names.
- The failures. A function that raises on bad input needs a test that it does, or one day it will not.
- One reason to fail. A test named
test_totalsthat checks six things tells you very little when it goes red.
Running them from inside Python
Failures and errors are different on purpose: a failure is your assertion refusing, an error is an exception nobody expected. A test suite full of errors is usually one broken import, not a hundred broken tests.
suite = unittest.TestLoader().loadTestsFromTestCase(TestTotal)
result = unittest.TextTestRunner(verbosity=0).run(suite)
result.testsRun # how many ran
result.wasSuccessful() # True if nothing failed or errored
result.failures # [(test, traceback), …] — an assertion said no
result.errors # [(test, traceback), …] — the code blew up