The Quiet Terminal

← Practical Tips

Your first automated test with pytest

2026-06-05 · 4 min read

I lost an afternoon once to a function that worked on the one input I kept re-testing by hand and broke on everything else. Manual testing has a hidden cost: it only covers whatever you remember to try, and after the fifth edit you stop remembering. An automated test is the same check, written down once, run every time. Here's the smallest useful version.

The function and the test

# math_utils.py
def add(a, b):
    return a + b

Beside it — literally in the same folder, named so it starts with test_:

# test_math_utils.py
from math_utils import add

def test_add():
    assert add(2, 3) == 5

That's the entire setup. No class, no imports of a testing framework, no ceremony. Run it:

pytest -q
.                                                                        [100%]
1 passed in 0.01s

pytest found the file on its own (anything named test_*.py), found the function (anything named test_*), and ran it. The -q keeps the output short; leave it off for the full report. Ten milliseconds is the other argument for tests: checking by hand was slower than the test will ever be.

The assert is the framework

There are no assertion methods to memorize — assert, the plain Python statement, is the whole API. But pytest rewrites your asserts under the hood so that a failure shows you real values. Break the test on purpose:

def test_add():
    assert add(2, 2) == 5
F                                                                        [100%]
================================== FAILURES ==================================
__________________________________ test_add __________________________________

    def test_add():
>       assert add(2, 2) == 5
E       assert 4 == 5
E        +  where 4 = add(2, 2)

test_math_utils.py:3: AssertionError
=========================== short test summary info ===========================
FAILED test_math_utils.py::test_add - assert 4 == 5

That E assert 4 == 5 line is why I stayed with pytest: it shows the actual result next to the expected one, plus the expression that produced it. Most failures explain themselves.

One test becomes many: parametrize

A teaser for the feature you'll reach for next. Three cases, one test function:

import pytest
from math_utils import add

@pytest.mark.parametrize("a, b, expected", [
    (2, 3, 5),
    (-1, 1, 0),
    (0, 0, 0),
])
def test_add(a, b, expected):
    assert add(a, b) == expected

Run it and pytest reports 3 passed — it generated a test per row. When one row fails, it names the exact row. Two things worth knowing as this grows. You can run one file (pytest test_math_utils.py -q) or filter by name (pytest -k add), which matters the day the suite takes real time. And when picking cases, start with the inputs you're slightly afraid of — the zero, the empty list, the negative number. The rows above are exactly that list for add. I put off automated tests for years because unittest's boilerplate made a five-line check feel like paperwork; pytest removed the excuse, and the reason left after that was just me.