Skip to content

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

items = [1, 2, 3]
assert len(items) > 5, f"expected > 5 items, got {len(items)}"

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:

python -m doctest -v analysis.py

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:

python -m unittest discover -s tests

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:

  1. Fail when behavior changes — test the contract, not the implementation.
  2. Are readable — the test name and body should explain the scenario.
  3. Are deterministic — no flaky randomness or wall-clock dependencies without control.
  4. Cover edge cases — empty input, None, duplicates, extremes, type boundaries.
  5. 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:

python -m unittest test_trimmed_mean
  • 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.