Set up pytest 8.x with Python for unit and integration testing using fixtures (scope, autouse, parametrize), async tests (pytest-asyncio), mocking (unittest.mock, pytest-mock), coverage (pytest-cov), conftest.py patterns, and markers.
84
81%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Risky
Do not use without reviewing
Set up pytest 8.x with Python for unit and integration testing using fixtures (scope, autouse, parametrize), async tests (pytest-asyncio), mocking (unittest.mock, pytest-mock), coverage (pytest-cov), conftest.py patterns, and markers.
pip install pytest pytest-asyncio pytest-mock pytest-cov
# Optional plugins
pip install pytest-xdist # Parallel test execution
pip install pytest-randomly # Randomize test order
pip install factory-boy # Test data factories
pip install httpx # Async HTTP client for testing FastAPIapp/
__init__.py
main.py
services/
user_service.py
repositories/
user_repository.py
tests/
__init__.py
conftest.py # Root-level fixtures and configuration
unit/
__init__.py
conftest.py # Unit-test-specific fixtures
test_user_service.py
integration/
__init__.py
conftest.py # Integration-test-specific fixtures (DB, API client)
test_user_api.py
factories.py # Test data factories
pyproject.toml # pytest configurationtest_. Test functions start with test_.conftest.py files for shared fixtures. Place them at the appropriate directory level (root for global, subdirectory for scoped).setUp/tearDown methods.@pytest.mark.parametrize for data-driven tests instead of duplicating test functions.@pytest.mark.asyncio for async test functions.@pytest.mark.slow, @pytest.mark.integration).pyproject.toml)[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"integration: marks tests as integration tests",
]
addopts = [
"--strict-markers",
"--strict-config",
"-ra",
"--tb=short",
]
filterwarnings = [
"error",
"ignore::DeprecationWarning",
]tests/conftest.py)import pytest
from unittest.mock import AsyncMock
@pytest.fixture
def mock_user():
"""A sample user dict for testing."""
return {
"id": 1,
"email": "test@example.com",
"name": "Test User",
"role": "user",
}
@pytest.fixture
def mock_users(mock_user):
"""A list of sample users."""
return [
mock_user,
{"id": 2, "email": "admin@example.com", "name": "Admin User", "role": "admin"},
]tests/unit/test_user_service.py)import pytest
from unittest.mock import AsyncMock, patch
from app.services.user_service import UserService
class TestUserService:
@pytest.fixture(autouse=True)
def setup(self):
self.mock_repo = AsyncMock()
self.service = UserService(repository=self.mock_repo)
async def test_get_user_returns_user_when_found(self, mock_user):
self.mock_repo.find_by_id.return_value = mock_user
result = await self.service.get_user(1)
assert result == mock_user
self.mock_repo.find_by_id.assert_called_once_with(1)
async def test_get_user_raises_when_not_found(self):
self.mock_repo.find_by_id.return_value = None
with pytest.raises(ValueError, match="User not found"):
await self.service.get_user(999)
async def test_create_user_hashes_password(self):
self.mock_repo.create.return_value = {"id": 1, "email": "new@example.com"}
await self.service.create_user(
email="new@example.com",
name="New User",
password="plaintext123",
)
call_args = self.mock_repo.create.call_args[1]
assert call_args["password"] != "plaintext123"import pytest
from app.utils.validators import validate_email
@pytest.mark.parametrize(
"email, expected",
[
("user@example.com", True),
("user.name@domain.co.uk", True),
("invalid", False),
("@no-local.com", False),
("no-at-sign.com", False),
("", False),
],
ids=[
"valid-basic",
"valid-subdomain",
"no-at-sign-no-domain",
"no-local-part",
"missing-at",
"empty-string",
],
)
def test_validate_email(email: str, expected: bool):
assert validate_email(email) is expectedimport pytest
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
@pytest.fixture(scope="session")
def engine():
"""Create engine once for the entire test session."""
engine = create_async_engine("postgresql+asyncpg://test:test@localhost:5432/testdb")
yield engine
# Cleanup runs after all tests complete
@pytest.fixture(scope="function")
async def session(engine):
"""Create a new session per test, rolled back after each test."""
async_session = async_sessionmaker(engine, class_=AsyncSession)
async with async_session() as session:
async with session.begin():
yield session
await session.rollback()def test_send_notification_calls_email_service(mocker):
mock_send = mocker.patch("app.services.email_service.send_email", return_value=True)
mocker.patch("app.services.email_service.get_template", return_value="<html>Hello</html>")
from app.services.notification_service import notify_user
notify_user("user@example.com", "Welcome!")
mock_send.assert_called_once_with(
to="user@example.com",
subject="Welcome!",
body="<html>Hello</html>",
)import pytest
import httpx
from app.main import app # FastAPI app
@pytest.fixture
async def client():
"""Async HTTP client for FastAPI integration tests."""
async with httpx.AsyncClient(app=app, base_url="http://test") as client:
yield client
class TestUserAPI:
async def test_create_user(self, client: httpx.AsyncClient):
response = await client.post(
"/api/users",
json={"email": "new@example.com", "name": "New User", "password": "secret123"},
)
assert response.status_code == 201
data = response.json()
assert data["email"] == "new@example.com"
assert "password" not in data
async def test_get_user_not_found(self, client: httpx.AsyncClient):
response = await client.get("/api/users/99999")
assert response.status_code == 404
assert response.json()["detail"] == "User not found"tests/factories.py)import factory
from app.models.user import User
class UserFactory(factory.Factory):
class Meta:
model = User
id = factory.Sequence(lambda n: n + 1)
email = factory.LazyAttribute(lambda obj: f"user{obj.id}@example.com")
name = factory.Faker("name")
role = "user"
# Usage in tests:
# user = UserFactory()
# admin = UserFactory(role="admin", email="admin@test.com")
# users = UserFactory.build_batch(5)import pytest
@pytest.mark.slow
def test_complex_computation():
"""This test takes a long time."""
result = expensive_computation()
assert result > 0
@pytest.mark.integration
async def test_database_query(session):
"""This test requires a real database."""
users = await session.execute(select(User))
assert users.scalars().all() is not Nonetests/integration/conftest.py)import pytest
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from app.database import Base
from app.models import * # noqa: ensure models are loaded
@pytest.fixture(scope="session")
async def engine():
engine = create_async_engine("postgresql+asyncpg://test:test@localhost:5432/testdb")
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield engine
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await engine.dispose()
@pytest.fixture
async def session(engine):
session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with session_factory() as session:
yield session
await session.rollback()responses library)import responses
@responses.activate
def test_external_api_call():
responses.add(
responses.GET,
"https://api.example.com/users",
json=[{"id": 1, "name": "Alice"}],
status=200,
)
result = fetch_users() # Your function that calls the external API
assert len(result) == 1
assert result[0]["name"] == "Alice"
@responses.activate
def test_external_api_error():
responses.add(
responses.GET,
"https://api.example.com/users",
json={"error": "Service unavailable"},
status=503,
)
with pytest.raises(ExternalServiceError):
fetch_users()# Install the responses library
pip install responses# Run all tests
pytest
# Run with verbose output
pytest -v
# Run a specific file
pytest tests/unit/test_user_service.py
# Run a specific test function
pytest tests/unit/test_user_service.py::TestUserService::test_get_user_returns_user_when_found
# Run tests matching a keyword
pytest -k "test_create"
# Run tests by marker
pytest -m "not slow"
pytest -m integration
# Run with coverage
pytest --cov=app --cov-report=term-missing --cov-report=html
# Run in parallel (pytest-xdist)
pytest -n auto
# Run with detailed failure output
pytest --tb=long
# Stop on first failure
pytest -x
# Re-run only failed tests
pytest --lf
# Show slowest 10 tests
pytest --durations=10
# Generate JUnit XML report (for CI)
pytest --junitxml=report.xmlhttpx.AsyncClient(app=app) for async integration tests. Override dependencies with app.dependency_overrides for mocking database sessions.sqlalchemy-starter skill. Use session-scoped engine fixture and function-scoped session with rollback.github-actions-ci skill. Run pytest --cov --junitxml=report.xml in CI. Upload coverage artifacts.docker compose to spin up test databases. Set DATABASE_URL as an environment variable in CI.mypy. Run mypy separately from pytest (they serve different purposes).# docker-compose.test.yml
services:
test-db:
image: postgres:16-alpine
environment:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports:
- "5433:5432"
tmpfs:
- /var/lib/postgresql/data # RAM-backed for speedUse DATABASE_URL=postgresql+asyncpg://test:test@localhost:5433/testdb in your test environment. Start with docker compose -f docker-compose.test.yml up -d before running integration tests.
181fcbc
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.