Skip to main content
Advertisement

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

FeatureUsage
Basic assertassert result == expected
Exception testingpytest.raises(ExcType)
Floating pointpytest.approx(value)
Show print outputpytest -s
Specific testpytest -k "test_name"
Stop on first failurepytest -x

pytest's greatest strength is providing detailed diffs on failure while using standard assert statements as-is.

Advertisement