pytest — Testing Like a Professional
Intermediatepytest turns plain assert into a test framework: test_ functions, parametrize for tables of cases, fixtures for setup/teardown, and raises for exception paths.
Overview
pytest is how Python teams verify code: no classes required, plain assert with intelligent failure output, discovery by naming convention (test_*.py, test_* functions). Its two superpowers: @pytest.mark.parametrize runs one test over a table of inputs (10 cases, 3 lines), and fixtures inject reusable setup (a temp DB, a client, sample data) with automatic cleanup — the same dependency-injection thinking FastAPI uses. Interviewers increasingly ask "how would you test this?" — this is the answer.
Tests, Assertions & Exception Paths
Name it test_*, assert what should hold, and pytest reports rich diffs on failure. pytest.raises asserts that the error path errors.
# calculator.py
def apply_discount(price, percent):
if not 0 <= percent <= 100:
raise ValueError("percent must be 0-100")
return round(price * (1 - percent / 100), 2)
# test_calculator.py
import pytest
from calculator import apply_discount
def test_basic_discount():
assert apply_discount(1000, 10) == 900.0
def test_zero_percent_returns_price():
assert apply_discount(500, 0) == 500.0
def test_invalid_percent_raises():
with pytest.raises(ValueError, match="0-100"):
apply_discount(1000, 150)
# $ pytest -v
# test_calculator.py::test_basic_discount PASSED
# ...
# On failure, pytest shows values:
# assert apply_discount(1000, 10) == 901
# AssertionError: assert 900.0 == 901parametrize & Fixtures
parametrize turns a table of (input, expected) into individual test cases with their own pass/fail. Fixtures provide dependencies by parameter name; yield fixtures clean up after.
import pytest
@pytest.mark.parametrize("price,pct,expected", [
(1000, 10, 900.0),
(1000, 0, 1000.0),
(999, 50, 499.5),
(1, 100, 0.0),
])
def test_discount_table(price, pct, expected):
from calculator import apply_discount
assert apply_discount(price, pct) == expected
# runs as 4 separate tests — failures pinpoint the exact row
@pytest.fixture
def db():
conn = {"students": []} # setup (imagine a real DB)
yield conn # test runs here
conn.clear() # teardown — ALWAYS runs
def test_enroll(db): # fixture injected by name
db["students"].append("asha")
assert len(db["students"]) == 1
# tmp_path — built-in fixture: a fresh temp directory per test
def test_writes_report(tmp_path):
out = tmp_path / "report.txt"
out.write_text("ok")
assert out.read_text() == "ok"
# conftest.py holds shared fixtures for the whole test folderKey Points to Remember
- 1Discovery by convention: test_*.py files, test_* functions, plain assert
- 2pytest.raises(Error, match=...) tests failure paths explicitly
- 3parametrize = table-driven tests; each row reports separately
- 4Fixtures inject setup by parameter name; yield fixtures guarantee teardown; conftest.py shares them
Interview Questions
Sign in to ask AriaHow does pytest discover tests? What makes it lighter than unittest?
What are fixtures and how do they handle teardown?
Test a function that should raise on bad input — show the pattern.
Ask Aria about pytest — Testing Like a Professional
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.