Skip to content
  • Testing: unittest & pytest · Python

Testing: unittest & pytest

LearnViaNotes @learnvianotes· 4 min read

Automated tests prove your code works today and keep it working tomorrow. Python ships unittest and doctest; most projects use pytest for its plain asserts and fixtures.

Why and what to test

A good test follows Arrange → Act → Assert: set up inputs, call the code once, check the result.

assert — the building block

Python
def slugify(title: str) -> str:
    return "-".join(title.lower().split())

# Arrange, Act, Assert — a test is just code that raises if something is wrong
assert slugify("Hello World") == "hello-world"
assert slugify("  Python   Notes ") == "python-notes"
try:
    assert slugify("A B") == "a_b", "separator should be a hyphen"
except AssertionError as e:
    print("failed:", e)             # failed: separator should be a hyphen

unittest — the built-in framework

Python
import io, unittest

class Stack:
    def __init__(self):
        self._items = []
    def push(self, x):
        self._items.append(x)
    def pop(self):
        if not self._items:
            raise IndexError("pop from empty stack")
        return self._items.pop()

class StackTest(unittest.TestCase):
    def setUp(self):                         # runs before EACH test method
        self.s = Stack()

    def test_push_then_pop(self):
        self.s.push(1)
        self.s.push(2)
        self.assertEqual(self.s.pop(), 2)

    def test_pop_empty_raises(self):
        with self.assertRaises(IndexError):
            self.s.pop()

    def test_many_values(self):
        for value in [0, "x", None]:
            with self.subTest(value=value):  # each case reported separately
                self.s.push(value)
                self.assertIs(self.s.pop(), value)

suite = unittest.defaultTestLoader.loadTestsFromTestCase(StackTest)
result = unittest.TextTestRunner(stream=io.StringIO(), verbosity=0).run(suite)
print(result.testsRun, len(result.failures), len(result.errors))   # 3 0 0
print(result.wasSuccessful())                                        # True
AssertionChecks
assertEqual(a, b) / assertNotEquala == b
assertTrue(x) / assertFalse(x)Truthiness
assertIs(a, b) / assertIsNone(x)Identity
assertIn(a, b)Membership
assertRaises(E)The block raises E (use as a context manager)
assertAlmostEqual(a, b, places=7)Floats close enough
assertCountEqual(a, b)Same items, any order

pytest — less boilerplate

Python
# test_cart.py
import pytest
from shop.cart import Cart

@pytest.fixture
def cart():                                   # fresh object for every test that asks for it
    c = Cart()
    c.add("pen", price=10, qty=2)
    return c

def test_total(cart):
    assert cart.total() == 20                 # plain assert — pytest shows both sides on failure

@pytest.mark.parametrize("qty, expected", [(1, 10), (3, 30), (0, 0)])
def test_line_total(qty, expected):
    c = Cart()
    c.add("pen", price=10, qty=qty)
    assert c.total() == expected

def test_negative_qty_rejected():
    with pytest.raises(ValueError, match="qty"):
        Cart().add("pen", price=10, qty=-1)

def test_export(cart, tmp_path):              # built-in fixture: a fresh temp directory
    path = tmp_path / "cart.json"
    cart.save(path)
    assert path.exists()
Terminal
$ pip install pytest
$ pytest                       # finds test_*.py files and test_* functions
$ pytest -q -k "total"         # quiet, only tests whose name matches
$ pytest -x --lf               # stop at first failure; rerun last failures
$ pytest --cov=shop            # coverage report (pytest-cov plugin)
pytest output
============================= test session starts ==============================
collected 6 items

test_cart.py ......                                                      [100%]

============================== 6 passed in 0.02s ===============================
unittestpytest
InstallBuilt inpip install pytest
StyleClasses + self.assertX methodsPlain functions + assert
SetupsetUp / tearDownFixtures, injected by name, with scopes
ParametrisesubTest@pytest.mark.parametrize
Runs the other's tests?NoYes — pytest runs unittest suites too

doctest — examples that are tests

Python
import doctest, io

def median(xs):
    """Return the median of a non-empty list.

    >>> median([3, 1, 2])
    2
    >>> median([4, 1, 3, 2])
    2.5
    """
    s = sorted(xs)
    mid = len(s) // 2
    return s[mid] if len(s) % 2 else (s[mid - 1] + s[mid]) / 2

runner = doctest.DocTestRunner()
for test in doctest.DocTestFinder().find(median, globs={"median": median}):
    runner.run(test, out=io.StringIO().write)
print(runner.tries, runner.failures)       # 2 0
Terminal
$ python -m doctest -v stats.py     # run every docstring example in a module
$ pytest --doctest-modules          # or let pytest collect them

Coverage & the TDD loop

  1. Red

    Write a small failing test for the next bit of behaviour.

  2. Green

    Write the simplest code that makes it pass.

  3. Refactor

    Clean up with the tests as a safety net; rerun them.

What is a fixture in pytest?

unittest or pytest?

How do you test code that raises an exception?

Free for students and teachers

Write notes like this one

Cards for every idea, quizzes and flashcards, read aloud — on the web and your phone.

Something wrong with this note? Open it to report