pytest-backend-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePytest Backend Testing Guidelines
Pytest后端测试指南
Purpose
目的
Complete guide for writing comprehensive tests for FastAPI backend applications using pytest, pytest-asyncio, and FastAPI TestClient. Emphasizes async testing, proper mocking, layered testing (repository → service → router), and achieving high test coverage.
本指南是使用pytest、pytest-asyncio和FastAPI TestClient为FastAPI后端应用编写全面测试的完整指引。重点介绍异步测试、正确的模拟方法、分层测试(数据仓库→服务→路由)以及实现高测试覆盖率的方法。
When to Use This Skill
适用场景
- Writing new test files for backend code
- Testing repositories, services, or API routes
- Setting up test fixtures and mocks
- Debugging failing tests
- Improving test coverage
- Writing async tests with pytest-asyncio
- Testing database operations
- Using FastAPI TestClient for route testing
- 为后端代码编写新的测试文件
- 测试数据仓库、服务或API路由
- 设置测试夹具和模拟对象
- 调试失败的测试用例
- 提升测试覆盖率
- 使用pytest-asyncio编写异步测试
- 测试数据库操作
- 使用FastAPI TestClient进行路由测试
Quick Start
快速入门
New Test File Checklist
新测试文件检查清单
Creating tests for new code? Follow this checklist:
- Create test file:
tests/unit/{domain}/test_{module}.py - Import pytest and pytest-asyncio
- Set up necessary fixtures (session, client, etc.)
- Use for async tests
@pytest.mark.asyncio - Follow AAA pattern: Arrange, Act, Assert
- Mock external dependencies
- Test both success and error cases
- Verify coverage meets 80% threshold
- Use descriptive test names:
test_<what>_<when>_<expected>
为新代码创建测试?请遵循以下检查清单:
- 创建测试文件:
tests/unit/{domain}/test_{module}.py - 导入pytest和pytest-asyncio
- 设置必要的夹具(会话、客户端等)
- 为异步测试使用装饰器
@pytest.mark.asyncio - 遵循AAA模式:准备(Arrange)、执行(Act)、断言(Assert)
- 模拟外部依赖
- 测试成功和错误两种场景
- 验证覆盖率达到80%的阈值
- 使用描述性测试名称:
test_<测试对象>_<场景>_<预期结果>
Test Coverage Checklist
测试覆盖率检查清单
Ensuring good coverage? Check these:
- Test all public methods/functions
- Test error handling and exceptions
- Test edge cases and boundary conditions
- Test validation logic
- Mock external dependencies (database, APIs)
- Verify async/await behavior
- Run
pytest --cov=backend --cov-report=term-missing - Check coverage report for gaps
- Aim for 80%+ coverage
确保良好的测试覆盖率?请检查以下内容:
- 测试所有公共方法/函数
- 测试错误处理和异常情况
- 测试边缘案例和边界条件
- 测试验证逻辑
- 模拟外部依赖(数据库、API)
- 验证异步/等待行为
- 运行命令:
pytest --cov=backend --cov-report=term-missing - 检查覆盖率报告中的空白点
- 目标覆盖率达到80%以上
Project Testing Structure
项目测试结构
Your qwarty backend testing structure:
backend/
tests/
conftest.py # Global fixtures
unit/
domain/
artist/
test_artist_repository.py
test_artist_service.py
artwork/
auth/
...
middleware/
test_error_handler.py
utils/
test_utils.py
integration/ # End-to-end tests
test_artist_api.py
test_auth_flow.py你的qwarty后端测试结构如下:
backend/
tests/
conftest.py # 全局夹具
unit/
domain/
artist/
test_artist_repository.py
test_artist_service.py
artwork/
auth/
...
middleware/
test_error_handler.py
utils/
test_utils.py
integration/ # 端到端测试
test_artist_api.py
test_auth_flow.pyCommon Test Patterns Quick Reference
常见测试模式速查
Basic Async Test
基础异步测试
python
import pytest
from sqlmodel.ext.asyncio.session import AsyncSession
@pytest.mark.asyncio
async def test_get_artist_by_id(db_session: AsyncSession):
# Arrange
artist_id = "test-artist-id"
# Act
result = await repository.get_by_id(artist_id)
# Assert
assert result is not None
assert result.id == artist_idpython
import pytest
from sqlmodel.ext.asyncio.session import AsyncSession
@pytest.mark.asyncio
async def test_get_artist_by_id(db_session: AsyncSession):
# Arrange
artist_id = "test-artist-id"
# Act
result = await repository.get_by_id(artist_id)
# Assert
assert result is not None
assert result.id == artist_idMocking Database Session
模拟数据库会话
python
from unittest.mock import AsyncMock, MagicMock
@pytest.mark.asyncio
async def test_create_artist_success():
# Arrange
mock_session = AsyncMock(spec=AsyncSession)
mock_session.execute = AsyncMock()
mock_session.commit = AsyncMock()
# Act
service = ArtistService(mock_session)
result = await service.create_artist(data)
# Assert
assert mock_session.commit.calledpython
from unittest.mock import AsyncMock, MagicMock
@pytest.mark.asyncio
async def test_create_artist_success():
# Arrange
mock_session = AsyncMock(spec=AsyncSession)
mock_session.execute = AsyncMock()
mock_session.commit = AsyncMock()
# Act
service = ArtistService(mock_session)
result = await service.create_artist(data)
# Assert
assert mock_session.commit.calledTesting FastAPI Routes
测试FastAPI路由
python
from fastapi.testclient import TestClient
from backend.main import create_application
@pytest.fixture
def client():
app = create_application()
return TestClient(app)
def test_get_artist_endpoint(client):
# Act
response = client.get("/api/v1/artists/test-id")
# Assert
assert response.status_code == 200
assert response.json()["id"] == "test-id"python
from fastapi.testclient import TestClient
from backend.main import create_application
@pytest.fixture
def client():
app = create_application()
return TestClient(app)
def test_get_artist_endpoint(client):
# Act
response = client.get("/api/v1/artists/test-id")
# Assert
assert response.status_code == 200
assert response.json()["id"] == "test-id"Test Organization Principles
测试组织原则
Test Structure (AAA Pattern)
测试结构(AAA模式)
- Arrange: Set up test data, mocks, fixtures
- Act: Execute the code under test
- Assert: Verify the expected outcome
- 准备(Arrange):设置测试数据、模拟对象、夹具
- 执行(Act):运行待测试的代码
- 断言(Assert):验证预期结果
Test Naming Convention
测试命名规范
python
undefinedpython
undefinedPattern: test_<what><when><expected>
模式:test_<测试对象><场景><预期结果>
def test_create_artist_with_valid_data_returns_artist()
def test_get_artist_when_not_found_raises_not_found_error()
def test_update_artist_with_duplicate_name_raises_conflict_error()
undefineddef test_create_artist_with_valid_data_returns_artist()
def test_get_artist_when_not_found_raises_not_found_error()
def test_update_artist_with_duplicate_name_raises_conflict_error()
undefinedTest Organization
测试组织方式
- Unit tests: Test individual functions/methods in isolation
- Integration tests: Test multiple components working together
- Group related tests: Use test classes for related functionality
- 单元测试:孤立测试单个函数/方法
- 集成测试:测试多个组件协同工作的情况
- 分组相关测试:使用测试类管理相关功能的测试
Topic Guides
主题指南
🏗️ Testing Architecture
🏗️ 测试架构
Three-Layer Testing Strategy:
- Repository Layer: Test database queries, CRUD operations
- Service Layer: Test business logic, orchestration
- Router Layer: Test API endpoints, request/response handling
Key Concepts:
- Mock dependencies at layer boundaries
- Test each layer independently
- Use integration tests for end-to-end flows
- Maintain test isolation
📖 Complete Guide: resources/testing-architecture.md
三层测试策略:
- 数据仓库层:测试数据库查询、CRUD操作
- 服务层:测试业务逻辑、编排流程
- 路由层:测试API端点、请求/响应处理
核心概念:
- 在层边界处模拟依赖
- 独立测试每一层
- 使用集成测试验证端到端流程
- 保持测试隔离性
📖 完整指南:resources/testing-architecture.md
🧪 Unit Testing
🧪 单元测试
Unit Test Best Practices:
- Test single responsibility
- Mock external dependencies
- Fast execution (no database, no network)
- Independent and isolated
- Test both success and failure paths
Unit Test Pattern:
python
@pytest.mark.asyncio
async def test_artist_service_create():
# Mock repository
mock_repo = AsyncMock()
mock_repo.create = AsyncMock(return_value=artist_model)
# Test service logic
service = ArtistService(mock_repo)
result = await service.create_artist(data)
assert result.name == data.name📖 Complete Guide: resources/unit-testing.md
单元测试最佳实践:
- 测试单一职责
- 模拟外部依赖
- 执行速度快(无需数据库、网络)
- 独立且隔离
- 测试成功和失败两种路径
单元测试模式:
python
@pytest.mark.asyncio
async def test_artist_service_create():
# 模拟数据仓库
mock_repo = AsyncMock()
mock_repo.create = AsyncMock(return_value=artist_model)
# 测试服务逻辑
service = ArtistService(mock_repo)
result = await service.create_artist(data)
assert result.name == data.name📖 完整指南:resources/unit-testing.md
🔗 Integration Testing
🔗 集成测试
Integration Test Focus:
- Test multiple components together
- Use real database (test database)
- Verify end-to-end workflows
- Test API contracts
Integration Test Pattern:
python
@pytest.mark.asyncio
async def test_create_artist_flow(db_session, client):
# Full flow: API → Service → Repository → DB
response = client.post("/api/v1/artists", json=artist_data)
assert response.status_code == 201
# Verify in database
artist = await db_session.get(Artist, response.json()["id"])
assert artist is not None📖 Complete Guide: resources/integration-testing.md
集成测试重点:
- 测试多个组件协同工作的情况
- 使用真实数据库(测试库)
- 验证端到端工作流
- 测试API契约
集成测试模式:
python
@pytest.mark.asyncio
async def test_create_artist_flow(db_session, client):
# 完整流程:API → 服务 → 数据仓库 → 数据库
response = client.post("/api/v1/artists", json=artist_data)
assert response.status_code == 201
# 在数据库中验证
artist = await db_session.get(Artist, response.json()["id"])
assert artist is not None📖 完整指南:resources/integration-testing.md
⚡ Async Testing
⚡ 异步测试
Async Test Patterns:
- Use decorator
@pytest.mark.asyncio - Configure pytest-asyncio in conftest.py
- Mock async functions with AsyncMock
- Test async context managers
- Handle async exceptions
Async Mock Pattern:
python
from unittest.mock import AsyncMock
@pytest.mark.asyncio
async def test_async_function():
mock_func = AsyncMock(return_value="result")
result = await mock_func()
assert result == "result"
mock_func.assert_awaited_once()📖 Complete Guide: resources/async-testing.md
异步测试模式:
- 使用装饰器
@pytest.mark.asyncio - 在conftest.py中配置pytest-asyncio
- 使用AsyncMock模拟异步函数
- 测试异步上下文管理器
- 处理异步异常
异步模拟模式:
python
from unittest.mock import AsyncMock
@pytest.mark.asyncio
async def test_async_function():
mock_func = AsyncMock(return_value="result")
result = await mock_func()
assert result == "result"
mock_func.assert_awaited_once()📖 完整指南:resources/async-testing.md
🎭 Mocking & Fixtures
🎭 模拟与夹具
Mocking Strategy:
- Mock external dependencies (database, APIs, S3)
- Use pytest fixtures for reusable test data
- Mock at layer boundaries
- Use MagicMock for sync, AsyncMock for async
Fixture Pattern:
python
import pytest
@pytest.fixture
def sample_artist():
return Artist(
id="test-id",
name="Test Artist",
bio="Test bio"
)
@pytest.fixture
async def db_session():
# Setup test database session
async with get_test_session() as session:
yield session
await session.rollback()📖 Complete Guide: resources/mocking-fixtures.md
模拟策略:
- 模拟外部依赖(数据库、API、S3)
- 使用pytest夹具复用测试数据
- 在层边界处进行模拟
- 同步代码使用MagicMock,异步代码使用AsyncMock
夹具模式:
python
import pytest
@pytest.fixture
def sample_artist():
return Artist(
id="test-id",
name="Test Artist",
bio="Test bio"
)
@pytest.fixture
async def db_session():
# 设置测试数据库会话
async with get_test_session() as session:
yield session
await session.rollback()📖 完整指南:resources/mocking-fixtures.md
📊 Coverage Best Practices
📊 覆盖率最佳实践
Coverage Strategy:
- Aim for 80%+ coverage (project requirement)
- Focus on critical business logic
- Test error paths and edge cases
- Use coverage reports to find gaps
- Exclude non-testable code (config, main.py)
Coverage Commands:
bash
undefined覆盖率策略:
- 目标覆盖率80%以上(项目要求)
- 重点覆盖关键业务逻辑
- 测试错误路径和边缘案例
- 使用覆盖率报告发现空白点
- 排除不可测试代码(配置文件、main.py)
覆盖率命令:
bash
undefinedRun tests with coverage
运行测试并生成覆盖率报告
pytest --cov=backend --cov-report=term-missing
pytest --cov=backend --cov-report=term-missing
Generate HTML report
生成HTML格式报告
pytest --cov=backend --cov-report=html
pytest --cov=backend --cov-report=html
Check coverage threshold
检查覆盖率阈值
pytest --cov=backend --cov-fail-under=80
**[📖 Complete Guide: resources/coverage-best-practices.md](resources/coverage-best-practices.md)**
---pytest --cov=backend --cov-fail-under=80
**[📖 完整指南:resources/coverage-best-practices.md](resources/coverage-best-practices.md)**
---🚀 FastAPI Testing
🚀 FastAPI测试
FastAPI Test Patterns:
- Use TestClient for route testing
- Test request validation
- Test response serialization
- Test authentication/authorization
- Test error handling middleware
TestClient Pattern:
python
from fastapi.testclient import TestClient
def test_create_artist_endpoint(client: TestClient):
response = client.post(
"/api/v1/artists",
json={"name": "Artist", "bio": "Bio"}
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "Artist"📖 Complete Guide: resources/fastapi-testing.md
FastAPI测试模式:
- 使用TestClient进行路由测试
- 测试请求验证
- 测试响应序列化
- 测试认证/授权
- 测试错误处理中间件
TestClient模式:
python
from fastapi.testclient import TestClient
def test_create_artist_endpoint(client: TestClient):
response = client.post(
"/api/v1/artists",
json={"name": "Artist", "bio": "Bio"}
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "Artist"📖 完整指南:resources/fastapi-testing.md
Navigation Guide
导航指南
| Need to... | Read this resource |
|---|---|
| Understand test structure | testing-architecture.md |
| Write unit tests | unit-testing.md |
| Write integration tests | integration-testing.md |
| Test async code | async-testing.md |
| Use mocks and fixtures | mocking-fixtures.md |
| Improve coverage | coverage-best-practices.md |
| Test FastAPI routes | fastapi-testing.md |
| 需求场景 | 参考资源 |
|---|---|
| 理解测试结构 | testing-architecture.md |
| 编写单元测试 | unit-testing.md |
| 编写集成测试 | integration-testing.md |
| 测试异步代码 | async-testing.md |
| 使用模拟和夹具 | mocking-fixtures.md |
| 提升测试覆盖率 | coverage-best-practices.md |
| 测试FastAPI路由 | fastapi-testing.md |
Core Principles
核心原则
- Test Isolation: Each test runs independently, no shared state
- AAA Pattern: Arrange, Act, Assert for clear test structure
- Async Testing: Use pytest-asyncio for async code
- Mock Dependencies: Mock external systems (database, APIs)
- Layered Testing: Test each layer (repository, service, router) separately
- Coverage Goals: Aim for 80%+ coverage, focus on business logic
- Descriptive Names: Clear test names explain what, when, expected
- Error Testing: Test both success and failure paths
- Fast Tests: Unit tests should be fast (no real database)
- Fixtures: Use fixtures for reusable test data and setup
- 测试隔离:每个测试独立运行,无共享状态
- AAA模式:采用准备、执行、断言的清晰测试结构
- 异步测试:使用pytest-asyncio处理异步代码
- 模拟依赖:模拟外部系统(数据库、API)
- 分层测试:分别测试每一层(数据仓库、服务、路由)
- 覆盖率目标:目标覆盖率80%以上,重点覆盖业务逻辑
- 描述性命名:清晰的测试名称说明测试对象、场景和预期结果
- 错误测试:测试成功和失败两种路径
- 快速测试:单元测试应快速执行(无需真实数据库)
- 夹具复用:使用夹具复用测试数据和设置
Quick Reference: Test Template
速查:测试模板
python
"""Tests for Artist domain."""
import pytest
from unittest.mock import AsyncMock, MagicMock
from sqlmodel.ext.asyncio.session import AsyncSession
from backend.domain.artist.service import ArtistService
from backend.domain.artist.repository import ArtistRepository
from backend.domain.artist.model import Artist
from backend.dtos.artist import ArtistRequestDto
from backend.error import NotFoundError
@pytest.fixture
def sample_artist():
"""Fixture for sample artist data."""
return Artist(
id="test-artist-id",
name="Test Artist",
bio="Test bio"
)
@pytest.fixture
def mock_session():
"""Fixture for mocked database session."""
return AsyncMock(spec=AsyncSession)
class TestArtistRepository:
"""Test suite for ArtistRepository."""
@pytest.mark.asyncio
async def test_get_by_id_success(self, mock_session, sample_artist):
"""Test get_by_id returns artist when found."""
# Arrange
mock_result = MagicMock()
mock_result.scalar_one_or_none.return_value = sample_artist
mock_session.execute = AsyncMock(return_value=mock_result)
repository = ArtistRepository(mock_session)
# Act
result = await repository.get_by_id("test-artist-id")
# Assert
assert result is not None
assert result.id == sample_artist.id
assert result.name == sample_artist.name
@pytest.mark.asyncio
async def test_get_by_id_not_found(self, mock_session):
"""Test get_by_id returns None when not found."""
# Arrange
mock_result = MagicMock()
mock_result.scalar_one_or_none.return_value = None
mock_session.execute = AsyncMock(return_value=mock_result)
repository = ArtistRepository(mock_session)
# Act
result = await repository.get_by_id("nonexistent-id")
# Assert
assert result is None
class TestArtistService:
"""Test suite for ArtistService."""
@pytest.mark.asyncio
async def test_create_artist_success(self, mock_session, sample_artist):
"""Test create_artist creates and returns artist."""
# Arrange
mock_repo = AsyncMock()
mock_repo.create = AsyncMock(return_value=sample_artist)
service = ArtistService(mock_session)
service._repository = mock_repo
request_dto = ArtistRequestDto(
name="Test Artist",
bio="Test bio"
)
# Act
result = await service.create_artist(request_dto)
# Assert
assert result.name == request_dto.name
mock_repo.create.assert_awaited_once()python
"""艺术家领域测试。"""
import pytest
from unittest.mock import AsyncMock, MagicMock
from sqlmodel.ext.asyncio.session import AsyncSession
from backend.domain.artist.service import ArtistService
from backend.domain.artist.repository import ArtistRepository
from backend.domain.artist.model import Artist
from backend.dtos.artist import ArtistRequestDto
from backend.error import NotFoundError
@pytest.fixture
def sample_artist():
"""示例艺术家数据的夹具。"""
return Artist(
id="test-artist-id",
name="Test Artist",
bio="Test bio"
)
@pytest.fixture
def mock_session():
"""模拟数据库会话的夹具。"""
return AsyncMock(spec=AsyncSession)
class TestArtistRepository:
"""ArtistRepository测试套件。"""
@pytest.mark.asyncio
async def test_get_by_id_success(self, mock_session, sample_artist):
"""测试找到艺术家时get_by_id返回对应对象。"""
# Arrange
mock_result = MagicMock()
mock_result.scalar_one_or_none.return_value = sample_artist
mock_session.execute = AsyncMock(return_value=mock_result)
repository = ArtistRepository(mock_session)
# Act
result = await repository.get_by_id("test-artist-id")
# Assert
assert result is not None
assert result.id == sample_artist.id
assert result.name == sample_artist.name
@pytest.mark.asyncio
async def test_get_by_id_not_found(self, mock_session):
"""测试未找到艺术家时get_by_id返回None。"""
# Arrange
mock_result = MagicMock()
mock_result.scalar_one_or_none.return_value = None
mock_session.execute = AsyncMock(return_value=mock_result)
repository = ArtistRepository(mock_session)
# Act
result = await repository.get_by_id("nonexistent-id")
# Assert
assert result is None
class TestArtistService:
"""ArtistService测试套件。"""
@pytest.mark.asyncio
async def test_create_artist_success(self, mock_session, sample_artist):
"""测试create_artist成功创建并返回艺术家对象。"""
# Arrange
mock_repo = AsyncMock()
mock_repo.create = AsyncMock(return_value=sample_artist)
service = ArtistService(mock_session)
service._repository = mock_repo
request_dto = ArtistRequestDto(
name="Test Artist",
bio="Test bio"
)
# Act
result = await service.create_artist(request_dto)
# Assert
assert result.name == request_dto.name
mock_repo.create.assert_awaited_once()Current Project Configuration
当前项目配置
Your qwarty backend test setup:
pytest.ini (in pyproject.toml):
toml
[tool.pytest.ini_options]
testpaths = ["tests"]
filterwarnings = ["ignore::DeprecationWarning"]
markers = [
"security: marks tests as security tests (SQL injection, etc.)",
"performance: marks tests as performance benchmarks",
]
addopts = [
"--cov=backend",
"--cov-report=term-missing",
"--cov-report=html",
"--cov-fail-under=80",
]Test Dependencies:
- pytest 8.4.2+
- pytest-asyncio 0.24.0+
- pytest-cov 6.0.0+
Coverage Exclusions:
- Tests themselves ()
tests/* - files
__init__.py - Main application entry ()
backend/main.py - Some routers and specific domains (see pyproject.toml)
你的qwarty后端测试配置如下:
pytest.ini(在pyproject.toml中):
toml
[tool.pytest.ini_options]
testpaths = ["tests"]
filterwarnings = ["ignore::DeprecationWarning"]
markers = [
"security: marks tests as security tests (SQL injection, etc.)",
"performance: marks tests as performance benchmarks",
]
addopts = [
"--cov=backend",
"--cov-report=term-missing",
"--cov-report=html",
"--cov-fail-under=80",
]测试依赖:
- pytest 8.4.2+
- pytest-asyncio 0.24.0+
- pytest-cov 6.0.0+
覆盖率排除项:
- 测试文件本身()
tests/* - 文件
__init__.py - 主应用入口()
backend/main.py - 部分路由和特定领域代码(详见pyproject.toml)
Related Skills
相关技能
- fastapi-backend-guidelines: Backend development patterns (what you're testing)
- error-tracking: Error handling patterns to test
Skill Status: Modular structure with progressive loading for optimal context management
- fastapi-backend-guidelines:后端开发模式(即你要测试的内容)
- error-tracking:待测试的错误处理模式
技能状态:模块化结构,支持渐进式加载,实现最优上下文管理