A test is a small program that runs your program and complains if the answer is wrong. The reason to write them is not correctness today — it is the freedom to change things tomorrow.
assert
The simplest test is a claim that must hold.
def with_tax(amount, rate=0.2):
return round(amount * (1 + rate), 2)
assert with_tax(100) == 120.0
assert with_tax(100, 0.1) == 110.0
assert with_tax(0) == 0.0
print("all assertions passed")
assert with_tax(19.99) == 24.0, f"got {with_tax(19.99)} instead"An assert that holds does nothing at all; one that fails raises
AssertionError with your message. That last line fails on purpose — and the
message tells you the actual value, which a bare assertion would not.
assert is for tests, not for validation
Python removes every assert when run with the -O flag, so never use one to
check user input or enforce security. For those, raise ValueError — Day 5.
Writing tests as functions
Group related assertions into named functions, so a failure says what broke.
def with_tax(amount, rate=0.2):
return round(amount * (1 + rate), 2)
def test_default_rate():
assert with_tax(100) == 120.0
def test_custom_rate():
assert with_tax(100, 0.1) == 110.0
def test_zero():
assert with_tax(0) == 0.0
def test_rounds_to_cents():
assert with_tax(19.99) == 23.99
tests = [test_default_rate, test_custom_rate, test_zero, test_rounds_to_cents]
passed = 0
for test in tests:
try:
test()
passed += 1
print(f" PASS {test.__name__}")
except AssertionError as error:
print(f" FAIL {test.__name__}: {error or 'assertion failed'}")
print(f"{passed}/{len(tests)} passed")That loop is, in miniature, what a test framework does.
unittest
unittest ships with Python, so it is always available.
import unittest
def with_tax(amount, rate=0.2):
if amount < 0:
raise ValueError("amount cannot be negative")
return round(amount * (1 + rate), 2)
class TestWithTax(unittest.TestCase):
def test_default_rate(self):
self.assertEqual(with_tax(100), 120.0)
def test_custom_rate(self):
self.assertEqual(with_tax(100, 0.1), 110.0)
def test_rounds_to_cents(self):
self.assertAlmostEqual(with_tax(19.99), 23.99)
def test_rejects_negative(self):
with self.assertRaises(ValueError):
with_tax(-1)
# In a file you would finish with unittest.main(). This page runs inside a
# browser runtime with no script module for it to inspect, so the suite is
# loaded and run explicitly instead.
suite = unittest.TestLoader().loadTestsFromTestCase(TestWithTax)
unittest.TextTestRunner(verbosity=2).run(suite)Useful assertions: assertEqual, assertTrue, assertIn, assertIsNone,
assertAlmostEqual (for floats — remember Day 1), and assertRaises as a
context manager for expected errors.
setUp
setUp runs before every test method, so each test starts from a clean state.
import unittest
class Cart:
def __init__(self):
self.items = []
def add(self, name, price):
self.items.append((name, price))
def total(self):
return round(sum(price for _, price in self.items), 2)
class TestCart(unittest.TestCase):
def setUp(self):
self.cart = Cart() # a brand new cart for every test
def test_starts_empty(self):
self.assertEqual(self.cart.total(), 0)
self.assertEqual(len(self.cart.items), 0)
def test_adds_items(self):
self.cart.add("keyboard", 49.99)
self.cart.add("mouse", 25.50)
self.assertEqual(self.cart.total(), 75.49)
def test_isolated_from_other_tests(self):
self.assertEqual(len(self.cart.items), 0)
suite = unittest.TestLoader().loadTestsFromTestCase(TestCart)
unittest.TextTestRunner(verbosity=2).run(suite)Tests must not depend on each other or on their order. setUp is how you
guarantee that.
pytest
Outside the standard library, almost everyone uses pytest. There is no class
and no special assertion methods — plain functions and plain assert:
# test_tax.py
import pytest
from tax import with_tax
def test_default_rate():
assert with_tax(100) == 120.0
def test_rejects_negative():
with pytest.raises(ValueError):
with_tax(-1)
@pytest.mark.parametrize("amount,expected", [
(0, 0.0),
(100, 120.0),
(19.99, 23.99),
])
def test_various_amounts(amount, expected):
assert with_tax(amount) == expected
Install it with pip install pytest, then run pytest in your project folder;
it finds files named test_*.py and functions named test_* by itself.
What to test
Test the edges, not the middle. For a function, that usually means:
- A typical case that should obviously work.
- Zero, empty, one — the boundaries.
- Something invalid, and the error it should raise.
- Any bug you have ever fixed, so it cannot come back.
Write the test when you fix the bug
The moment a bug is fresh is the cheapest moment to capture it. A test written then is worth three written later from memory.
Test a function you did not write
initials("ada lovelace") should return "A.L.". Write tests covering a
normal name, a single name, an empty string, and extra whitespace — then fix
the function so they all pass.
def initials(full_name):
parts = full_name.split(" ")
return ".".join(part[0].upper() for part in parts) + "."
def check(description, actual, expected):
status = "PASS" if actual == expected else f"FAIL (got {actual!r})"
print(f" {status} {description}")
check("normal name", initials("ada lovelace"), "A.L.")
# Add your checks hereShow one solution
def initials(full_name):
"""Return dotted initials, e.g. 'ada lovelace' -> 'A.L.'."""
parts = full_name.split() # split() with no argument handles
if not parts: # runs of whitespace and empty input
return ""
return ".".join(part[0].upper() for part in parts) + "."
def check(description, actual, expected):
status = "PASS" if actual == expected else f"FAIL (got {actual!r})"
print(f" {status} {description}")
check("normal name", initials("ada lovelace"), "A.L.")
check("single name", initials("grace"), "G.")
check("empty string", initials(""), "")
check("extra whitespace", initials(" ada lovelace "), "A.L.")
check("three names", initials("ada byron lovelace"), "A.B.L.")Two bugs surfaced. split(" ") on " ada " produces empty strings, and
part[0] on an empty string raises IndexError; bare split() handles both.
And the empty input needed a decision — returning "" rather than "." — which
is exactly the kind of question writing tests forces you to answer.
What you learned
assert claim, messageis the smallest possible test.- Never use
assertfor validating real input — it can be optimised away. unittestgives you test classes,setUp, and assertions likeassertRaises.pytestdoes the same with plain functions and plainassert.- Test the boundaries, the errors, and every bug you fix.