关于 pytest
面向 Python 开发者的测试框架,适用于从单元测试到复杂集成测试的场景。它提供 fixture、参数化、标记和插件等机制,并支持异步测试与并行执行。
主要功能
- 01使用 fixture 管理测试依赖与清理
- 02通过参数化运行数据驱动测试
- 03用标记组织并筛选测试
- 04支持异步测试与并行执行
- 05借助插件扩展覆盖率和模拟能力
适用场景
- 01为 Python 函数编写单元测试
- 02测试 FastAPI、Django 或 Flask 应用
- 03运行异步及集成测试
- 04生成覆盖率报告并按标记筛选测试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ bobmatnyc/ claude-mpm-skills --skill pytest安装后在 Claude Code 中输入/pytest确认已加载。
附带文件metadata.json
详细介绍
pytest - 专业 Python 测试
概述
pytest 是行业标准的 Python 测试框架,提供 fixture、参数化、标记、插件等强大功能,并可与 FastAPI、Django 和 Flask 无缝集成。它提供简单且可扩展的测试方式,适用于从单元测试到复杂集成场景的各种测试。
主要功能:
- 用于依赖注入的 fixture 系统
- 用于数据驱动测试的参数化功能
- 丰富的断言内省(无需使用
self.assertEqual) - 插件生态系统(pytest-cov、pytest-asyncio、pytest-mock、pytest-django)
- 支持 Async/await
- 使用 pytest-xdist 并行执行测试
- 测试发现与组织
- 详尽的失败报告
安装:
# Basic pytest
pip install pytest
# With common plugins
pip install pytest pytest-cov pytest-asyncio pytest-mock
# For FastAPI testing
pip install pytest httpx pytest-asyncio
# For Django testing
pip install pytest pytest-django
# For async databases
pip install pytest-asyncio aiosqlite
基本测试模式
1. 简单测试函数
# test_math.py
def add(a, b):
return a + b
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
assert add(0, 0) == 0
def test_add_negative():
assert add(-2, -3) == -5
运行测试:
# Discover and run all tests
pytest
# Verbose output
pytest -v
# Show print statements
pytest -s
# Run specific test file
pytest test_math.py
# Run specific test function
pytest test_math.py::test_add
2. 用测试类进行组织
# test_calculator.py
class Calculator:
def add(self, a, b):
return a + b
def multiply(self, a, b):
return a * b
class TestCalculator:
def test_add(self):
calc = Calculator()
assert calc.add(2, 3) == 5
def test_multiply(self):
calc = Calculator()
assert calc.multiply(4, 5) == 20
def test_add_negative(self):
calc = Calculator()
assert calc.add(-1, -1) == -2
3. 断言与预期失败
import pytest
# Test exception raising
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
def test_divide_by_zero():
with pytest.raises(ValueError, match="Cannot divide by zero"):
divide(10, 0)
def test_divide_success():
assert divide(10, 2) == 5.0
# Test approximate equality
def test_float_comparison():
assert 0.1 + 0.2 == pytest.approx(0.3)
# Test containment
def test_list_contains():
result = [1, 2, 3, 4]
assert 3 in result
assert len(result) == 4
Fixtures——依赖注入
基本 Fixtures
# conftest.py
import pytest
@pytest.fixture
def sample_data():
"""Provide sample data for tests."""
return {"name": "Alice", "age": 30, "email": "[email protected]"}
@pytest.fixture
def empty_list():
"""Provide an empty list."""
return []
# test_fixtures.py
def test_sample_data(sample_data):
assert sample_data["name"] == "Alice"
assert sample_data["age"] == 30
def test_empty_list(empty_list):
empty_list.append(1)
assert len(empty_list) == 1
Fixture 作用域
import pytest
# Function scope (default) - runs for each test
@pytest.fixture(scope="function")
def user():
return {"id": 1, "name": "Alice"}
# Class scope - runs once per test class
@pytest.fixture(scope="class")
def database():
db = setup_database()
yield db
db.close()
# Module scope - runs once per test module
@pytest.fixture(scope="module")
def api_client():
client = APIClient()
yield client
client.shutdown()
# Session scope - runs once for entire test session
@pytest.fixture(scope="session")
def app_config():
return load_config()
Fixture 的设置与拆卸
import pytest
import tempfile
import shutil
@pytest.fixture
def temp_directory():
"""Create a temporary directory for test."""
temp_dir = tempfile.mkdtemp()
print(f"
Setup: Created {temp_dir}")
yield temp_dir # Provide directory to test
# Teardown: cleanup after test
shutil.rmtree(temp_dir)
print(f"
Teardown: Removed {temp_dir}")
def test_file_creation(temp_directory):
file_path = f"{temp_directory}/test.txt"
with open(file_path, "w") as f:
f.write("test content")
assert os.path.exists(file_path)
Fixture 依赖关系
import pytest
@pytest.fixture
def database_connection():
"""Database connection."""
conn = connect_to_db()
yield conn
conn.close()
@pytest.fixture
def database_session(database_connection):
"""Database session depends on connection."""
session = create_session(database_connection)
yield session
session.rollback()
session.close()
@pytest.fixture
def user_repository(database_session):
"""User repository depends on session."""
return UserRepository(database_session)
def test_create_user(user_repository):
user = user_repository.create(name="Alice", email="[email protected]")
assert user.name == "Alice"
参数化——数据驱动测试
基本参数化
import pytest
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5),
(5, 7, 12),
(-1, 1, 0),
(0, 0, 0),
(100, 200, 300),
])
def test_add_parametrized(a, b, expected):
assert add(a, b) == expected
多个参数
@pytest.mark.parametrize("operation,a,b,expected", [
("add", 2, 3, 5),
("subtract", 10, 5, 5),
("multiply", 4, 5, 20),
("divide", 10, 2, 5),
])
def test_calculator_operations(operation, a, b, expected):
calc = Calculator()
result = getattr(calc, operation)(a, b)
assert result == expected
带 ID 的参数化
@pytest.mark.parametrize("input_data,expected", [
pytest.param({"name": "Alice"}, "Alice", id="valid_name"),
pytest.param({"name": ""}, None, id="empty_name"),
pytest.param({}, None, id="missing_name"),
], ids=lambda x: x if isinstance(x, str) else None)
def test_extract_name(input_data, expected):
result = extract_name(input_data)
assert result == expected
间接参数化(Fixtures)
@pytest.fixture
def user_data(request):
"""Create user based on parameter."""
return {"name": request.param, "email": f"{request.param}@example.com"}
@pytest.mark.parametrize("user_data", ["Alice", "Bob", "Charlie"], indirect=True)
def test_user_creation(user_data):
assert "@example.com" in user_data["email"]
测试标记
内置标记
import pytest
# Skip test
@pytest.mark.skip(reason="Not implemented yet")
def test_future_feature():
pass
# Skip conditionally
@pytest.mark.skipif(sys.platform == "win32", reason="Unix-only test")
def test_unix_specific():
pass
# Expected failure
@pytest.mark.xfail(reason="Known bug #123")
def test_known_bug():
assert False
# Slow test marker
@pytest.mark.slow
def test_expensive_operation():
time.sleep(5)
assert True
自定义标记
# pytest.ini
[pytest]
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
integration: marks tests as integration tests
unit: marks tests as unit tests
smoke: marks tests as smoke tests
# test_custom_markers.py
import pytest
@pytest.mark.unit
def test_fast_unit():
assert True
@pytest.mark.integration
@pytest.mark.slow
def test_slow_integration():
# Integration test with database
pass
@pytest.mark.smoke
def test_critical_path():
# Smoke test for critical functionality
pass
按标记运行测试:
# Run only unit tests
pytest -m unit
# Run all except slow tests
pytest -m "not slow"
# Run integration tests
pytest -m integration
# Run unit AND integration
pytest -m "unit or integration"
# Run smoke tests only
pytest -m smoke
FastAPI 测试
基本 FastAPI 测试设置
# app/main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
@app.get("/")
def read_root():
return {"message": "Hello World"}
@app.get("/items/{item_id}")
def read_item(item_id: int):
if item_id == 0:
raise HTTPException(status_code=404, detail="Item not found")
return {"item_id": item_id, "name": f"Item {item_id}"}
@app.post("/items")
def create_item(item: Item):
return {"name": item.name, "price": item.price, "id": 123}
FastAPI 测试客户端
# conftest.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
@pytest.fixture
def client():
"""FastAPI test client."""
return TestClient(app)
# test_api.py
def test_read_root(client):
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
def test_read_item(client):
response = client.get("/items/1")
assert response.status_code == 200
assert response.json() == {"item_id": 1, "name": "Item 1"}
def test_read_item_not_found(client):
response = client.get("/items/0")
assert response.status_code == 404
assert response.json() == {"detail": "Item not found"}
def test_create_item(client):
response = client.post(
"/items",
json={"name": "Widget", "price": 9.99}
)
assert response.status_code == 200
data = response.json()
assert data["name"] == "Widget"
assert data["price"] == 9.99
assert "id" in data
FastAPI 异步测试
# conftest.py
import pytest
from httpx import AsyncClient
from app.main import app
@pytest.fixture
async def async_client():
"""Async test client for FastAPI."""
async with AsyncClient(app=app, base_url="http://test") as client:
yield client
# test_async_api.py
import pytest
@pytest.mark.asyncio
async def test_read_root_async(async_client):
response = await async_client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
@pytest.mark.asyncio
async def test_create_item_async(async_client):
response = await async_client.post(
"/items",
json={"name": "Gadget", "price": 19.99}
)
assert response.status_code == 200
assert response.json()["name"] == "Gadget"
使用数据库测试 FastAPI
# conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.database import Base, get_db
from app.main import app
# Test database
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
@pytest.fixture(scope="function")
def test_db():
"""Create test database."""
Base.metadata.create_all(bind=engine)
yield
Base.metadata.drop_all(bind=engine)
@pytest.fixture
def client(test_db):
"""Override database dependency."""
def override_get_db():
try:
db = TestingSessionLocal()
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as test_client:
yield test_client
app.dependency_overrides.clear()
# test_users.py
def test_create_user(client):
response = client.post(
"/users",
json={"email": "[email protected]", "password": "secret"} # pragma: allowlist secret
)
assert response.status_code == 200
assert response.json()["email"] == "[email protected]"
def test_read_users(client):
# Create user first
client.post("/users", json={"email": "[email protected]", "password": "pass1"}) # pragma: allowlist secret
client.post("/users", json={"email": "[email protected]", "password": "pass2"}) # pragma: allowlist secret
# Read users
response = client.get("/users")
assert response.status_code == 200
assert len(response.json()) == 2
Django 测试
Django pytest 配置
# pytest.ini
[pytest]
DJANGO_SETTINGS_MODULE = myproject.settings
python_files = tests.py test_*.py *_tests.py
# conftest.py
import pytest
from django.conf import settings
@pytest.fixture(scope='session')
def django_db_setup():
settings.DATABASES['default'] = {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
Django 模型测试
# models.py
from django.db import models
class User(models.Model):
email = models.EmailField(unique=True)
name = models.CharField(max_length=100)
is_active = models.BooleanField(default=True)
# test_models.py
import pytest
from myapp.models import User
@pytest.mark.django_db
def test_create_user():
user = User.objects.create(
email="[email protected]",
name="Test User"
)
assert user.email == "[email protected]"
assert user.is_active is True
@pytest.mark.django_db
def test_user_unique_email():
User.objects.create(email="[email protected]", name="User 1")
with pytest.raises(Exception): # IntegrityError
User.objects.create(email="[email protected]", name="User 2")
Django 视图测试
# views.py
from django.http import JsonResponse
from django.views import View
class UserListView(View):
def get(self, request):
users = User.objects.all()
return JsonResponse({
"users": list(users.values("id", "email", "name"))
})
# test_views.py
import pytest
from django.test import Client
from myapp.models import User
@pytest.fixture
def client():
return Client()
@pytest.mark.django_db
def test_user_list_view(client):
# Create test data
User.objects.create(email="[email protected]", name="User 1")
User.objects.create(email="[email protected]", name="User 2")
# Test view
response = client.get("/users/")
assert response.status_code == 200
data = response.json()
assert len(data["users"]) == 2
Django REST Framework 测试
# serializers.py
from rest_framework import serializers
from myapp.models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'email', 'name', 'is_active']
# views.py
from rest_framework import viewsets
from myapp.models import User
from myapp.serializers import UserSerializer
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
# test_api.py
import pytest
from rest_framework.test import APIClient
from myapp.models import User
@pytest.fixture
def api_client():
return APIClient()
@pytest.mark.django_db
def test_list_users(api_client):
User.objects.create(email="[email protected]", name="User 1")
User.objects.create(email="[email protected]", name="User 2")
response = api_client.get("/api/users/")
assert response.status_code == 200
assert len(response.data) == 2
@pytest.mark.django_db
def test_create_user(api_client):
data = {"email": "[email protected]", "name": "New User"}
response = api_client.post("/api/users/", data)
assert response.status_code == 201
assert User.objects.filter(email="[email protected]").exists()
模拟与打补丁
pytest-mock(pytest.fixture.mocker)
# Install: pip install pytest-mock
# service.py
import requests
def get_user_data(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
return response.json()
# test_service.py
def test_get_user_data(mocker):
# Mock requests.get
mock_response = mocker.Mock()
mock_response.json.return_value = {"id": 1, "name": "Alice"}
mocker.patch("requests.get", return_value=mock_response)
result = get_user_data(1)
assert result["name"] == "Alice"
模拟类方法
class UserService:
def get_user(self, user_id):
# Database call
return database.fetch_user(user_id)
def get_user_name(self, user_id):
user = self.get_user(user_id)
return user["name"]
def test_get_user_name(mocker):
service = UserService()
# Mock the get_user method
mocker.patch.object(
service,
"get_user",
return_value={"id": 1, "name": "Alice"}
)
result = service.get_user_name(1)
assert result == "Alice"
使用副作用进行模拟
def test_retry_on_failure(mocker):
# First call fails, second succeeds
mock_api = mocker.patch("requests.get")
mock_api.side_effect = [
requests.exceptions.Timeout(), # First call
mocker.Mock(json=lambda: {"status": "ok"}) # Second call
]
result = api_call_with_retry()
assert result["status"] == "ok"
assert mock_api.call_count == 2
监视调用
def test_function_called_correctly(mocker):
spy = mocker.spy(module, "function_name")
# Call code that uses the function
module.run_workflow()
# Verify it was called
assert spy.call_count == 1
spy.assert_called_once_with(arg1="value", arg2=42)
覆盖率与报告
pytest-cov 配置
# Install
pip install pytest-cov
# Run with coverage
pytest --cov=app --cov-report=html --cov-report=term
# Generate coverage report
pytest --cov=app --cov-report=term-missing
# Coverage with minimum threshold
pytest --cov=app --cov-fail-under=80
pytest.ini 覆盖率配置
# pytest.ini
[pytest]
addopts =
--cov=app
--cov-report=html
--cov-report=term-missing
--cov-fail-under=80
-v
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
覆盖率报告
# HTML report (opens in browser)
pytest --cov=app --cov-report=html
open htmlcov/index.html
# Terminal report with missing lines
pytest --cov=app --cov-report=term-missing
# XML report (for CI/CD)
pytest --cov=app --cov-report=xml
# JSON report
pytest --cov=app --cov-report=json
异步测试
pytest-asyncio
# Install: pip install pytest-asyncio
# conftest.py
import pytest
# Enable asyncio mode
pytest_plugins = ('pytest_asyncio',)
# async_service.py
import asyncio
import aiohttp
async def fetch_data(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.json()
# test_async_service.py
import pytest
@pytest.mark.asyncio
async def test_fetch_data(mocker):
# Mock aiohttp response
mock_response = mocker.AsyncMock()
mock_response.json.return_value = {"data": "test"}
mock_session = mocker.AsyncMock()
mock_session.__aenter__.return_value.get.return_value.__aenter__.return_value = mock_response
mocker.patch("aiohttp.ClientSession", return_value=mock_session)
result = await fetch_data("https://api.example.com/data")
assert result["data"] == "test"
异步 Fixtures
@pytest.fixture
async def async_db_session():
"""Async database session."""
async with async_engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async with AsyncSession(async_engine) as session:
yield session
async with async_engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
@pytest.mark.asyncio
async def test_create_user_async(async_db_session):
user = User(email="[email protected]", name="Test")
async_db_session.add(user)
await async_db_session.commit()
result = await async_db_session.execute(
select(User).where(User.email == "[email protected]")
)
assert result.scalar_one().name == "Test"
本地 pytest 配置(你的代码库)
项目 pyproject.toml 中的常见设置:
asyncio_mode = "auto"(mcp-browser、mcp-memory、claude-mpm、edgar 中的默认值)addopts包含--strict-markers和--strict-config,以确保 CI 一致性- 覆盖率标志:
--cov=<package>、--cov-report=term-missing、--cov-report=xml - 选择性忽略(mcp-vector-search):
--ignore=tests/manual、--ignore=tests/e2e pythonpath = ["src"],用于解析可编辑安装的导入(mcp-ticketer)
常见标记:
unit、integration、e2eslow、benchmark、performancerequires_api(edgar)
参考:如需完整列表,请参阅 claude-mpm、edgar、mcp-vector-search、mcp-ticketer 和 kuzu-memory 中的 pyproject.toml。
最佳实践
1. 测试组织
project/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── models.py
│ └── services.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Shared fixtures
│ ├── test_models.py # Model tests
│ ├── test_services.py # Service tests
│ ├── test_api.py # API tests
│ └── integration/
│ ├── __init__.py
│ └── test_workflows.py
└── pytest.ini
2. 命名约定
# ✅ GOOD: Clear test names
def test_user_creation_with_valid_email():
pass
def test_user_creation_raises_error_for_duplicate_email():
pass
# ❌ BAD: Vague names
def test_user1():
pass
def test_case2():
pass
3. Arrange-Act-Assert 模式
def test_user_service_creates_user():
# Arrange: Setup test data and dependencies
service = UserService(database=mock_db)
user_data = {"email": "[email protected]", "name": "Test"}
# Act: Perform the action being tested
result = service.create_user(user_data)
# Assert: Verify the outcome
assert result.email == "[email protected]"
assert result.id is not None
4. 使用 Fixtures 进行常见设置
# ❌ BAD: Repeated setup
def test_user_creation():
db = setup_database()
user = create_user(db)
assert user.id is not None
db.close()
def test_user_deletion():
db = setup_database()
user = create_user(db)
delete_user(db, user.id)
db.close()
# ✅ GOOD: Fixture-based setup
@pytest.fixture
def db():
database = setup_database()
yield database
database.close()
@pytest.fixture
def user(db):
return create_user(db)
def test_user_creation(user):
assert user.id is not None
def test_user_deletion(db, user):
delete_user(db, user.id)
assert not user_exists(db, user.id)
5. 对相似测试进行参数化
# ❌ BAD: Duplicate test code
def test_add_positive():
assert add(2, 3) == 5
def test_add_negative():
assert add(-2, -3) == -5
def test_add_zero():
assert add(0, 0) == 0
# ✅ GOOD: Parametrized tests
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5),
(-2, -3, -5),
(0, 0, 0),
])
def test_add(a, b, expected):
assert add(a, b) == expected
6. 每个测试只测一件事
# ❌ BAD: Testing multiple things
def test_user_workflow():
user = create_user()
assert user.id is not None
updated = update_user(user.id, name="New Name")
assert updated.name == "New Name"
deleted = delete_user(user.id)
assert deleted is True
# ✅ GOOD: Separate tests
def test_user_creation():
user = create_user()
assert user.id is not None
def test_user_update():
user = create_user()
updated = update_user(user.id, name="New Name")
assert updated.name == "New Name"
def test_user_deletion():
user = create_user()
result = delete_user(user.id)
assert result is True
7. 使用标记组织测试
@pytest.mark.unit
def test_pure_function():
pass
@pytest.mark.integration
@pytest.mark.slow
def test_database_integration():
pass
@pytest.mark.smoke
def test_critical_path():
pass
8. 模拟外部依赖
# ✅ GOOD: Mock external API
def test_fetch_user_data(mocker):
mocker.patch("requests.get", return_value=mock_response)
result = fetch_user_data(user_id=1)
assert result["name"] == "Alice"
# ❌ BAD: Real API call in test
def test_fetch_user_data():
result = fetch_user_data(user_id=1) # Real HTTP request!
assert result["name"] == "Alice"
常见陷阱
❌ 反模式 1:测试依赖于执行顺序
# WRONG: Tests should be independent
class TestUserWorkflow:
user_id = None
def test_create_user(self):
user = create_user()
TestUserWorkflow.user_id = user.id
def test_update_user(self):
# Fails if test_create_user didn't run first!
update_user(TestUserWorkflow.user_id, name="New")
正确写法:
@pytest.fixture
def created_user():
return create_user()
def test_create_user(created_user):
assert created_user.id is not None
def test_update_user(created_user):
update_user(created_user.id, name="New")
❌ 反模式 2:未清理资源
# WRONG: Database not cleaned up
def test_user_creation():
db = setup_database()
user = create_user(db)
assert user.id is not None
# Database connection not closed!
正确写法:
@pytest.fixture
def db():
database = setup_database()
yield database
database.close() # Cleanup
❌ 反模式 3:测试实现细节
# WRONG: Testing internal implementation
def test_user_service_uses_cache():
service = UserService()
service.get_user(1)
assert service._cache.has_key(1) # Testing internal cache!
正确写法:
# Test behavior, not implementation
def test_user_service_returns_user():
service = UserService()
user = service.get_user(1)
assert user.id == 1
❌ 反模式 4:不使用 pytest 功能
# WRONG: Using unittest assertions
import unittest
def test_addition():
result = add(2, 3)
unittest.TestCase().assertEqual(result, 5)
正确写法:
# Use pytest's rich assertions
def test_addition():
assert add(2, 3) == 5
❌ 反模式 5:Fixture 过于复杂
# WRONG: Fixture does too much
@pytest.fixture
def everything():
db = setup_db()
user = create_user(db)
session = login(user)
cache = setup_cache()
# ... too many things!
return {"db": db, "user": user, "session": session, "cache": cache}
正确写法:
# Separate, composable fixtures
@pytest.fixture
def db():
return setup_db()
@pytest.fixture
def user(db):
return create_user(db)
@pytest.fixture
def session(user):
return login(user)
快速参考
常用命令
# Run all tests
pytest
# Verbose output
pytest -v
# Show print statements
pytest -s
# Run specific file
pytest tests/test_api.py
# Run specific test
pytest tests/test_api.py::test_create_user
# Run by marker
pytest -m unit
pytest -m "not slow"
# Run with coverage
pytest --cov=app --cov-report=html
# Parallel execution
pytest -n auto # Requires pytest-xdist
# Stop on first failure
pytest -x
# Show local variables on failure
pytest -l
# Run last failed tests
pytest --lf
# Run failed tests first
pytest --ff
pytest.ini 模板
[pytest]
# Minimum pytest version
minversion = 7.0
# Test discovery patterns
python_files = test_*.py *_test.py
python_classes = Test*
python_functions = test_*
# Test paths
testpaths = tests
# Command line options
addopts =
-v
--strict-markers
--cov=app
--cov-report=html
--cov-report=term-missing
--cov-fail-under=80
# Markers
markers =
unit: Unit tests
integration: Integration tests
slow: Slow-running tests
smoke: Smoke tests for critical paths
# Django settings (if using Django)
DJANGO_SETTINGS_MODULE = myproject.settings
# Asyncio mode
asyncio_mode = auto
conftest.py 模板
# conftest.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
# FastAPI client fixture
@pytest.fixture
def client():
return TestClient(app)
# Database fixture
@pytest.fixture(scope="function")
def db():
database = setup_test_database()
yield database
database.close()
# Mock user fixture
@pytest.fixture
def mock_user():
return {"id": 1, "email": "[email protected]", "name": "Test User"}
# Custom pytest configuration
def pytest_configure(config):
config.addinivalue_line("markers", "api: API tests")
config.addinivalue_line("markers", "db: Database tests")
资源
- 官方文档:https://docs.pytest.org/
- pytest-asyncio:https://pytest-asyncio.readthedocs.io/
- pytest-cov:https://pytest-cov.readthedocs.io/
- pytest-mock:https://pytest-mock.readthedocs.io/
- pytest-django:https://pytest-django.readthedocs.io/
- FastAPI 测试:https://fastapi.tiangolo.com/tutorial/testing/
相关技能
使用 pytest 时,可以考虑搭配以下技能:
- fastapi-local-dev:FastAPI 开发服务器模式和测试夹具
- test-driven-development:完整的 TDD 工作流(RED/GREEN/REFACTOR 循环)
- systematic-debugging:调查失败测试的根本原因
TDD 工作流速查参考(内联以便独立使用)
RED → GREEN → REFACTOR 循环:
-
RED 阶段:编写失败的测试
def test_should_authenticate_user_when_credentials_valid(): # Test that describes desired behavior user = User(username='alice', password='secret123') result = authenticate(user) assert result.is_authenticated is True # This test will fail because authenticate() doesn't exist yet -
GREEN 阶段:让测试通过
def authenticate(user): # Minimum code to pass the test if user.username == 'alice' and user.password == 'secret123': # pragma: allowlist secret return AuthResult(is_authenticated=True) return AuthResult(is_authenticated=False) -
REFACTOR 阶段:改进代码
def authenticate(user): # Clean up while keeping tests green hashed_password = hash_password(user.password) stored_user = database.get_user(user.username) return AuthResult( is_authenticated=(stored_user.password_hash == hashed_password) )
测试结构:Arrange-Act-Assert(AAA)
def test_user_creation():
# Arrange: Set up test data
user_data = {'username': 'alice', 'email': '[email protected]'}
# Act: Perform the action
user = create_user(user_data)
# Assert: Verify outcome
assert user.username == 'alice'
assert user.email == '[email protected]'
调试速查参考(内联以便独立使用)
阶段 1:调查根本原因
- 完整阅读错误消息(堆栈跟踪、行号)
- 稳定复现(记录准确步骤)
- 检查近期更改(git log、git diff)
- 了解发生了哪些更改,以及这些更改为何可能导致故障
阶段 2:隔离问题
# Use pytest's built-in debugging
pytest tests/test_auth.py -vv --pdb # Drop into debugger on failure
pytest tests/test_auth.py -x # Stop on first failure
pytest tests/test_auth.py -k "auth" # Run only auth-related tests
# Add strategic print/logging
def test_complex_workflow():
user = create_user({'username': 'test'})
print(f"DEBUG: Created user {user.id}") # Visible with pytest -s
result = process_user(user)
print(f"DEBUG: Result status {result.status}")
assert result.success
阶段 3:修复根本原因
- 修复根本问题,而非表面症状
- 添加回归测试以防止问题再次发生
- 验证修复不会导致其他测试失败
阶段 4:验证解决方案
# Run full test suite
pytest
# Run with coverage
pytest --cov=src --cov-report=html
# Verify specific test patterns
pytest -k "auth or login" -v
[如果一并部署,可在相应技能中查看完整的 TDD 和调试工作流]
Python 代码质量反模式
待测试的代码越整洁,就越容易测试;而一些 Python 代码质量缺陷会直接导致测试不稳定或在未通过时仍悄然通过(例如过于宽泛的 except、格式错误的异常类、身份比较与相等比较混淆)。代码质量反模式现已移至专门的技能,不再属于此测试技能:
请参阅 python-code-quality 技能(toolchains/python/quality/code-quality),了解最重要的 Python 反模式——异常层次结构正确性、单例身份比较、窄范围异常处理、避免通配符导入、为魔术数字命名,以及移除无用的局部变量;其中包含不合规与合规示例,以及各项的测试方法。项目级、带严重性标签的审查清单请参阅 code-review-standards 技能。
**pytest 版本兼容性:**此技能涵盖 pytest 7.0+,反映了 2025 年 Python 测试的当前最佳实践。