pytest-backend-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Pytest 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
    @pytest.mark.asyncio
    for async tests
  • 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.py

Common 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_id
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_id

Mocking 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.called
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.called

Testing 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模式)

  1. Arrange: Set up test data, mocks, fixtures
  2. Act: Execute the code under test
  3. Assert: Verify the expected outcome
  1. 准备(Arrange):设置测试数据、模拟对象、夹具
  2. 执行(Act):运行待测试的代码
  3. 断言(Assert):验证预期结果

Test Naming Convention

测试命名规范

python
undefined
python
undefined

Pattern: 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()
undefined
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()
undefined

Test 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:
  1. Repository Layer: Test database queries, CRUD operations
  2. Service Layer: Test business logic, orchestration
  3. 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

三层测试策略:
  1. 数据仓库层:测试数据库查询、CRUD操作
  2. 服务层:测试业务逻辑、编排流程
  3. 路由层:测试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
    @pytest.mark.asyncio
    decorator
  • 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
undefined

Run 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 structuretesting-architecture.md
Write unit testsunit-testing.md
Write integration testsintegration-testing.md
Test async codeasync-testing.md
Use mocks and fixturesmocking-fixtures.md
Improve coveragecoverage-best-practices.md
Test FastAPI routesfastapi-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

核心原则

  1. Test Isolation: Each test runs independently, no shared state
  2. AAA Pattern: Arrange, Act, Assert for clear test structure
  3. Async Testing: Use pytest-asyncio for async code
  4. Mock Dependencies: Mock external systems (database, APIs)
  5. Layered Testing: Test each layer (repository, service, router) separately
  6. Coverage Goals: Aim for 80%+ coverage, focus on business logic
  7. Descriptive Names: Clear test names explain what, when, expected
  8. Error Testing: Test both success and failure paths
  9. Fast Tests: Unit tests should be fast (no real database)
  10. Fixtures: Use fixtures for reusable test data and setup

  1. 测试隔离:每个测试独立运行,无共享状态
  2. AAA模式:采用准备、执行、断言的清晰测试结构
  3. 异步测试:使用pytest-asyncio处理异步代码
  4. 模拟依赖:模拟外部系统(数据库、API)
  5. 分层测试:分别测试每一层(数据仓库、服务、路由)
  6. 覆盖率目标:目标覆盖率80%以上,重点覆盖业务逻辑
  7. 描述性命名:清晰的测试名称说明测试对象、场景和预期结果
  8. 错误测试:测试成功和失败两种路径
  9. 快速测试:单元测试应快速执行(无需真实数据库)
  10. 夹具复用:使用夹具复用测试数据和设置

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/*
    )
  • __init__.py
    files
  • 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:待测试的错误处理模式

技能状态:模块化结构,支持渐进式加载,实现最优上下文管理