Python Testing¶
Core testing concepts every Python developer should know — assertions, test organization, doctests,
unittest, and the habits that make tests useful.
Quick Reference¶
| Concept | What it checks |
|---|---|
assert |
A condition you expect to be true in normal code |
unittest.TestCase |
A reusable test harness with assertions and fixtures |
| Doctest | Example snippets in docstrings run as tests |
| Test discovery | pytest / unittest find tests by naming convention |
| Fixture | Setup/teardown shared by multiple tests |
| Mock | Replace a real object with a controlled stand-in |
| Coverage | Fraction of code exercised by the test run |
Assertions¶
The simplest test is a statement that something is true.
def normalize(values):
"""Center values around zero."""
mean = sum(values) / len(values)
return [v - mean for v in values]
# A quick inline check
assert normalize([1, 2, 3]) == [-1.0, 0.0, 1.0]
assert is fine in scripts and notebooks, but test runners give you better reporting, discovery, and teardown. The key rule: tests should fail for exactly one reason. One assertion per behavior, not one giant block.
Assertion messages¶
A good message explains the expectation, not just that something broke.
Test Organization¶
A common layout for small-to-medium projects:
my_project/
├── src/
│ └── analysis.py
└── tests/
├── __init__.py
├── test_analysis.py
└── test_integration.py
Rules of thumb:
- Mirror the source structure under tests/ when it helps navigation.
- Name test files test_*.py or *_test.py so discovery works.
- One test module per source module is a good default.
- Keep tests short; extract shared setup into fixtures or helper functions.
Doctests¶
Doctests run examples embedded in docstrings. Useful for tiny utilities and pure functions.
def clip(value, lo, hi):
"""
Clamp value into [lo, hi].
>>> clip(5, 0, 10)
5
>>> clip(-3, 0, 10)
0
>>> clip(12, 0, 10)
10
"""
return max(lo, min(value, hi))
Run with:
Doctests are best for illustrating and verifying simple behavior. They are not a substitute for a real test suite for non-trivial logic.
unittest¶
The standard library's unittest gives you a class-based structure.
import unittest
from analysis import normalize, clip
class TestNormalize(unittest.TestCase):
def test_center_simple(self):
result = normalize([1, 2, 3])
self.assertEqual(result, [-1.0, 0.0, 1.0])
def test_center_empty_raises(self):
with self.assertRaises(ZeroDivisionError):
normalize([])
def test_clip_range(self):
self.assertEqual(clip(5, 0, 10), 5)
self.assertEqual(clip(-3, 0, 10), 0)
self.assertEqual(clip(12, 0, 10), 10)
if __name__ == "__main__":
unittest.main()
Run:
unittest assertions: assertEqual, assertNotEqual, assertTrue, assertFalse, assertRaises, assertAlmostEqual, assertIn, assertIsNone, and more.
When to Use What¶
| Situation | Good fit |
|---|---|
| Quick sanity check during development | assert |
| Tiny pure function with readable examples | Doctest |
| Standard-library-only project | unittest |
| Larger project with fixtures and plugins | pytest (see sibling cheat sheet) |
Writing Useful Tests¶
A test that never fails is not helping. Aim for tests that:
- Fail when behavior changes — test the contract, not the implementation.
- Are readable — the test name and body should explain the scenario.
- Are deterministic — no flaky randomness or wall-clock dependencies without control.
- Cover edge cases — empty input, None, duplicates, extremes, type boundaries.
- Run fast — slow tests get skipped in practice.
Example of a boundary-focused set:
def test_clip_boundaries():
assert clip(0, 0, 10) == 0 # at lower bound
assert clip(10, 0, 10) == 10 # at upper bound
assert clip(-1, 0, 10) == 0 # just below
assert clip(11, 0, 10) == 10 # just above
Test Data¶
Keep test data small and explicit. Inline literals are often clearer than loading a big fixture file.
# Clear and self-contained
def test_first_and_last():
data = [10, 20, 30, 40]
assert first(data) == 10
assert last(data) == 40
For larger or shared data, use fixtures or factory helpers so the test body still reads as the scenario.
Parametrized-Style Thinking (Without pytest)¶
Even without pytest.mark.parametrize, the same idea helps: test several inputs in one place.
def test_clip_variants():
cases = [
(5, 0, 10, 5),
(-3, 0, 10, 0),
(12, 0, 10, 10),
(0, 0, 10, 0),
(10, 0, 10, 10),
]
for value, lo, hi, expected in cases:
assert clip(value, lo, hi) == expected
With pytest, this becomes more ergonomic — see the sibling cheat sheet.
Common Mistakes¶
- Testing private details — refactor breaks tests that assert internals. Test observable behavior.
- No assertions — a test that runs code but asserts nothing can pass while being useless.
- Over-mocking — mocking everything means you never test real integration.
- Huge tests — a test that covers 20 behaviors is hard to debug when it fails.
- Ignoring edge cases — empty collections, None, zero, negatives, large values.
- Relying on implicit state — tests should be isolated and repeatable.
Practical Example — Test a Small Analysis Function¶
Say you have a function that computes a trimmed mean.
def trimmed_mean(values, trim=1):
"""Mean after dropping `trim` items from each end."""
if not values or trim * 2 >= len(values):
raise ValueError("not enough values")
sorted_vals = sorted(values)
cut = sorted_vals[trim:-trim] if trim else sorted_vals
return sum(cut) / len(cut)
Tests:
import unittest
class TestTrimmedMean(unittest.TestCase):
def test_trim_one_from_three(self):
self.assertAlmostEqual(trimmed_mean([1, 2, 3], trim=1), 2.0)
def test_no_trim(self):
self.assertAlmostEqual(trimmed_mean([1, 2, 3], trim=0), 2.0)
def test_odd_number_of_values(self):
self.assertAlmostEqual(trimmed_mean([1, 5, 10, 20, 50], trim=1), 10.0)
def test_empty_raises(self):
with self.assertRaises(ValueError):
trimmed_mean([])
def test_trim_too_large_raises(self):
with self.assertRaises(ValueError):
trimmed_mean([1, 2], trim=2)
Run:
Related Cheat Sheets¶
- pytest — fixtures, parametrize, mocking, markers
- debugging — when tests fail, find out why fast
- logging — inspect runtime behavior alongside tests
- scikit-learn — many ML workflows are easier to test in small deterministic steps
Official Documentation¶
Last Updated¶
2026-09-06 — Initial version.