| name | testing-pytest |
| description | Pytest testing skill with modern patterns - use for writing Python tests with fixtures, parametrization, and modular design |
Pytest Testing Skill
Use this skill when writing Python tests with pytest framework.
Follow pytest best practices for modern, modular, and reusable test design.
Pytest is the standard. Use pytest for all Python testing.
Tests MUST be isolated. Each test should be independent and not rely on execution order or state from other tests.
Fixtures over setup/teardown. Use pytest fixtures for test dependencies and resource management, not setUp/tearDown methods.
Parametrize for data-driven tests. Use @pytest.mark.parametrize instead of writing multiple similar test functions.
Organize tests logically. Mirror the source code structure in your test directory. Group related tests in classes when appropriate.
Test behavior, not implementation. Focus on testing what the code does, not how it does it. This makes tests resilient to refactoring.
Pytest Basics
Test Discovery and Naming
Pytest automatically discovers tests following these conventions:
test_user.py
user_test.py
test_models.py
models.py
def test_user_creation():
pass
def test_invalid_email():
pass
def helper_function():
pass
class TestUser:
def test_creation(self):
pass
class TestUserValidation:
def test_email(self):
pass
class UserHelper:
pass
Basic Test Structure
from myapp.models import User
def test_user_creation():
"""Test that a user can be created with valid data."""
username = "alice"
email = "alice@example.com"
user = User(username=username, email=email)
assert user.username == "alice"
assert user.email == "alice@example.com"
assert user.is_active is True
def test_user_invalid_email():
"""Test that invalid email raises ValueError."""
with pytest.raises(ValueError, match="Invalid email"):
User(username="bob", email="not-an-email")
Running Tests
pytest
pytest tests/test_user.py
pytest tests/test_user.py::test_user_creation
pytest -k "user and not invalid"
pytest -v
pytest --cov=myapp --cov-report=html
pytest -x
pytest --lf
pytest -n auto
Assertions
Basic Assertions
import pytest
def test_assertions():
"""Demonstrate pytest assertion introspection."""
assert result == expected
assert user.name == "Alice"
assert value is None
assert obj is not None
assert "admin" in user.roles
assert item not in processed_items
assert age >= 18
assert count < MAX_COUNT
assert user.is_active
assert not user.is_deleted
assert isinstance(result, dict)
assert isinstance(user, User)
def test_approximate_equality():
"""Test floating point with tolerance."""
import math
result = math.sqrt(2) ** 2
assert result == pytest.approx(2.0, rel=1e-9)
def test_assertion_messages():
"""Add custom messages to assertions."""
assert user.age >= 18, f"User {user.name} is underage: {user.age}"
Exception Testing
import pytest
def test_exception_raised():
"""Test that exception is raised."""
with pytest.raises(ValueError):
process_invalid_data()
def test_exception_with_message():
"""Test exception message matches pattern."""
with pytest.raises(ValueError, match="Invalid email format"):
User(email="not-an-email")
def test_exception_details():
"""Test exception attributes."""
with pytest.raises(APIError) as exc_info:
call_api_with_invalid_token()
assert exc_info.value.status_code == 401
assert "authentication" in str(exc_info.value).lower()
def test_no_exception():
"""Test that no exception is raised."""
result = process_valid_data()
assert result is not None
Warnings Testing
import pytest
import warnings
def test_deprecated_function():
"""Test that deprecated function shows warning."""
with pytest.warns(DeprecationWarning, match="deprecated"):
old_function()
def test_warning_details():
"""Test warning details."""
with pytest.warns(UserWarning) as warning_list:
trigger_warning()
assert len(warning_list) == 1
assert "careful" in str(warning_list[0].message)
Fixtures
Basic Fixtures
import pytest
from myapp.database import Database
from myapp.models import User
@pytest.fixture
def database():
"""Provide a database connection for tests."""
db = Database(":memory:")
db.create_tables()
yield db
db.close()
@pytest.fixture
def sample_user():
"""Provide a sample user for tests."""
return User(username="alice", email="alice@example.com")
def test_user_save(database, sample_user):
"""Test saving user to database."""
database.save(sample_user)
retrieved = database.get_user(sample_user.id)
assert retrieved.username == "alice"
Fixture Scopes
import pytest
@pytest.fixture
def temp_file():
"""Create temp file for each test."""
file = create_temp_file()
yield file
file.delete()
@pytest.fixture(scope="class")
def database_connection():
"""Create database connection for test class."""
conn = Database.connect()
yield conn
conn.close()
@pytest.fixture(scope="module")
def api_client():
"""Create API client for entire module."""
client = APIClient()
client.authenticate()
yield client
client.logout()
@pytest.fixture(scope="session")
def docker_container():
"""Start Docker container for entire test session."""
container = start_docker_postgres()
yield container
container.stop()
Fixture Dependencies
import pytest
@pytest.fixture
def database():
"""Provide database connection."""
db = Database(":memory:")
db.create_tables()
yield db
db.close()
@pytest.fixture
def user_repository(database):
"""Provide user repository (depends on database fixture)."""
return UserRepository(database)
@pytest.fixture
def sample_users(user_repository):
"""Create sample users (depends on user_repository)."""
users = [
User(username="alice", email="alice@example.com"),
User(username="bob", email="bob@example.com"),
]
for user in users:
user_repository.save(user)
return users
def test_find_user(user_repository, sample_users):
"""Test finding user by username."""
user = user_repository.find_by_username("alice")
assert user is not None
assert user.email == "alice@example.com"
Autouse Fixtures
import pytest
@pytest.fixture(autouse=True)
def reset_state():
"""Reset global state before each test (runs automatically)."""
clear_cache()
reset_counters()
yield
@pytest.fixture(autouse=True, scope="module")
def setup_logging():
"""Configure logging for all tests in module."""
configure_test_logging()
Factory Fixtures
import pytest
@pytest.fixture
def user_factory(database):
"""Fixture that returns a factory function for creating users."""
def _create_user(username: str, **kwargs):
user = User(username=username, **kwargs)
database.save(user)
return user
return _create_user
def test_multiple_users(user_factory):
"""Test with multiple users created by factory."""
alice = user_factory("alice", email="alice@example.com")
bob = user_factory("bob", email="bob@example.com", admin=True)
assert alice.username == "alice"
assert bob.is_admin is True
Parametrization
Basic Parametrization
import pytest
@pytest.mark.parametrize("input,expected", [
(2, 4),
(3, 9),
(4, 16),
(5, 25),
])
def test_square(input, expected):
"""Test square function with multiple inputs."""
assert square(input) == expected
@pytest.mark.parametrize("email", [
"user@example.com",
"test+tag@domain.org",
"name.surname@company.co.uk",
])
def test_valid_emails(email):
"""Test that valid emails are accepted."""
assert is_valid_email(email) is True
@pytest.mark.parametrize("invalid_email", [
"not-an-email",
"@example.com",
"user@",
"user name@example.com",
])
def test_invalid_emails(invalid_email):
"""Test that invalid emails are rejected."""
assert is_valid_email(invalid_email) is False
Multiple Parameters
import pytest
@pytest.mark.parametrize("username,email,expected_valid", [
("alice", "alice@example.com", True),
("bob", "bob@example.com", True),
("", "test@example.com", False),
("alice", "not-an-email", False),
("a" * 100, "test@example.com", False),
])
def test_user_validation(username, email, expected_valid):
"""Test user validation with various inputs."""
user = User(username=username, email=email)
assert user.is_valid() == expected_valid
Parametrize with IDs
import pytest
@pytest.mark.parametrize("input,expected", [
pytest.param(2, 4, id="two"),
pytest.param(3, 9, id="three"),
pytest.param(10, 100, id="ten"),
])
def test_square_with_ids(input, expected):
"""Test square with readable test IDs."""
assert square(input) == expected
@pytest.mark.parametrize("value,result", [
(0, "zero"),
(1, "one"),
(5, "many"),
], ids=["zero", "one", "many"])
def test_number_name(value, result):
assert get_name(value) == result
Combining Parametrize
import pytest
@pytest.mark.parametrize("x", [1, 2])
@pytest.mark.parametrize("y", [3, 4])
def test_add(x, y):
"""Test all combinations: (1,3), (1,4), (2,3), (2,4)."""
result = x + y
assert result > 0
Parametrizing Fixtures
import pytest
@pytest.fixture(params=["sqlite", "postgres", "mysql"])
def database(request):
"""Parametrized fixture - runs tests with each database."""
db_type = request.param
db = Database.create(db_type)