Tests let you change code confidently; pytest is the de facto standard in the Python ecosystem.
Python ships unittest in the standard library, but most projects use pytest (pip install pytest) because tests are plain functions using plain assert. Put tests in files named test_*.py, functions named test_*, and run pytest from the project root. When an assertion fails, pytest rewrites it to show the values involved, so assert total == 10 reports what total actually was.
Core features:
pytest.raises(ValueError, match="positive")as a context manager asserts that code raises@pytest.mark.parametrize("a,b,expected", [...])runs one test over many input sets- fixtures are functions decorated with
@pytest.fixturethat provide setup (and teardown viayield); a test receives one by naming it as a parameter - built-in fixtures like
tmp_path(a fresh temporary directory) andmonkeypatch(temporarily patch attributes or environment variables) pytest.approxcompares floats with tolerance
Useful flags: -x stop at the first failure, -k name select tests by name, -q quiet, --lf rerun last failures.
unittest organises tests as methods on unittest.TestCase subclasses with methods like assertEqual and assertRaises; pytest runs those too, so you can mix. unittest.mock (Mock, patch) is the standard way to replace collaborators such as network calls, and works with either runner.
Aim tests at behaviour, not internals: test what a function returns or does for a given input, including edge cases (empty input, boundaries, invalid values). Fast, isolated tests are the ones people actually run.
Example
# test_pricing.py -- run with: pytest -q
import pytest
def apply_discount(price: float, percent: float) -> float:
if not 0 <= percent <= 100:
raise ValueError("percent must be between 0 and 100")
return round(price * (1 - percent / 100), 2)
def test_basic_discount():
assert apply_discount(100, 25) == 75
@pytest.mark.parametrize("price, percent, expected", [
(10, 0, 10), (10, 100, 0), (19.99, 10, 17.99),
])
def test_edges(price, percent, expected):
assert apply_discount(price, percent) == pytest.approx(expected)
def test_rejects_bad_percent():
with pytest.raises(ValueError, match="between 0 and 100"):
apply_discount(10, 150)
@pytest.fixture
def config_file(tmp_path): # tmp_path: built-in fixture
path = tmp_path / "shop.ini"
path.write_text("currency = EUR")
return path
def test_reads_config(config_file):
assert "EUR" in config_file.read_text()Remember
- pytest uses plain
assertwith detailed failure output - Use
pytest.raises,parametrizeand fixtures liketmp_path unittest.mockreplaces external dependencies in tests
Watch out for
- Testing private implementation details, so every refactor breaks the tests.