pytest Basics
pytest is Python's standard testing framework. It is widely used for its concise syntax, rich plugin ecosystem, and detailed failure reports.
Installation and First Test
# pip install pytest
# test_hello.py
def add(a: int, b: int) -> int:
return a + b
def test_add_positive():
assert add(1, 2) == 3
def test_add_negative():
assert add(-1, -2) == -3
def test_add_zero():
assert add(0, 0) == 0
# Run: pytest test_hello.py
# Run: pytest -v (verbose)
# Run: pytest -v -s (include print output)
Test Discovery Rules
project/
├── src/
│ └── calculator.py
└── tests/
├── conftest.py # shared fixtures
├── test_calculator.py # test_ prefix files
└── unit/
└── test_utils.py
# pytest auto-discovery rules:
# 1. test_*.py or *_test.py files
# 2. Classes with Test prefix (TestCalculator)
# 3. Functions/methods with test_ prefix
# test_calculator.py
class TestCalculator:
def test_add(self):
assert 1 + 1 == 2
def test_subtract(self):
assert 5 - 3 == 2
# No __init__ needed (recommended)
assert Rewriting — Detailed Failure Messages
# pytest rewrites assert statements to provide detailed info on failure
def test_list_comparison():
result = [1, 2, 3, 4]
expected = [1, 2, 3, 5]
assert result == expected
# On failure:
# AssertionError: assert [1, 2, 3, 4] == [1, 2, 3, 5]
# At index 3 diff: 4 != 5
def test_string_comparison():
result = "Hello World"
assert "Python" in result
# On failure:
# AssertionError: assert 'Python' in 'Hello World'
def test_dict_comparison():
result = {"name": "Alice", "age": 30}
expected = {"name": "Alice", "age": 31}
assert result == expected
# On failure: dict diff is displayed
# Custom message
def test_with_message():
value = 42
assert value > 100, f"Value {value} must be greater than 100"
Exception Testing
import pytest
def divide(a: float, b: float) -> float:
if b == 0:
raise ZeroDivisionError("Cannot divide by zero")
return a / b
def parse_age(value: str) -> int:
age = int(value)
if age < 0 or age > 150:
raise ValueError(f"Invalid age: {age}")
return age
# Confirm exception is raised
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
divide(1, 0)
# Check exception message
def test_divide_by_zero_message():
with pytest.raises(ZeroDivisionError, match="Cannot divide by zero"):
divide(1, 0)
# Inspect exception object
def test_invalid_age():
with pytest.raises(ValueError) as exc_info:
parse_age("200")
assert "200" in str(exc_info.value)
# Test that no exception is raised
def test_valid_divide():
result = divide(10, 2)
assert result == pytest.approx(5.0)
# Floating point comparison
def test_float_comparison():
assert 0.1 + 0.2 == pytest.approx(0.3)
assert 0.1 + 0.2 == pytest.approx(0.3, rel=1e-9)
assert 1.0 == pytest.approx(1.0 + 1e-8, abs=1e-7)
Test Structure Patterns
import pytest
# AAA pattern: Arrange, Act, Assert
def test_user_full_name():
# Arrange
first_name = "Alice"
last_name = "Smith"
# Act
full_name = f"{first_name} {last_name}"
# Assert
assert full_name == "Alice Smith"
# GWT pattern: Given, When, Then (BDD style)
def test_shopping_cart():
# Given
cart = []
item = {"name": "Python Book", "price": 35000}
# When
cart.append(item)
# Then
assert len(cart) == 1
assert cart[0]["name"] == "Python Book"
# Class-based tests (grouping related tests)
class TestBankAccount:
def test_initial_balance(self):
balance = 0
assert balance == 0
def test_deposit(self):
balance = 0
balance += 1000
assert balance == 1000
def test_withdraw(self):
balance = 5000
balance -= 2000
assert balance == 3000
pytest Options and Execution
# Basic execution
pytest # entire current directory
pytest tests/ # tests folder only
pytest tests/test_calc.py # specific file
pytest tests/test_calc.py::test_add # specific test
# Output options
pytest -v # verbose
pytest -s # allow print output
pytest -v --tb=short # short traceback
pytest --tb=no # no traceback
# Filtering
pytest -k "add or subtract" # names containing add or subtract
pytest -k "not slow" # exclude slow
pytest -m integration # only marked tests
# Failure control
pytest -x # stop on first failure
pytest --maxfail=3 # stop after 3 failures
pytest --lf # only last failed tests
pytest --ff # failed tests first
# Parallel execution (pip install pytest-xdist)
pytest -n auto # parallel by CPU count
pytest -n 4 # 4 workers
pytest.ini / pyproject.toml Configuration
# pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = "-v --tb=short"
markers = [
"slow: slow tests",
"integration: integration tests",
"unit: unit tests",
]
# pytest.ini (separate file)
[pytest]
testpaths = tests
addopts = -v --tb=short
markers =
slow: marks tests as slow
integration: marks integration tests
Summary
| Feature | Usage |
|---|---|
| Basic assert | assert result == expected |
| Exception testing | pytest.raises(ExcType) |
| Floating point | pytest.approx(value) |
| Show print output | pytest -s |
| Specific test | pytest -k "test_name" |
| Stop on first failure | pytest -x |
pytest's greatest strength is providing detailed diffs on failure while using standard assert statements as-is.