Home/Learn/Python A–Z/pytest — Testing Like a Professional

pytest — Testing Like a Professional

Intermediate
Important Libraries

pytest 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.

Plain assert + raises — that's the whole API
# 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 == 901

parametrize & 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.

One test, many cases; setup via injection
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 folder

Key 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 Aria
1

How does pytest discover tests? What makes it lighter than unittest?

EasyThoughtworks
2

What are fixtures and how do they handle teardown?

MediumTransUnion
3

Test a function that should raise on bad input — show the pattern.

MediumAmazon

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.

Loading discussion…