FastAPI如何用契约测试确保API的「菜单」与「菜品」一致?

73 阅读8分钟

一、契约测试:API交互的「合同」保障

1.1 什么是契约测试?

契约测试(Contract Testing)是一种验证API提供者(如FastAPI服务)与消费者(如前端、其他微服务)之间交互一致性的测试方法。它的核心是一份「契约」——定义了API的请求格式、响应结构、参数约束等规则,双方必须严格遵守。
打个比方:你去餐厅吃饭,菜单(契约)上写着「番茄鸡蛋面」包含番茄、鸡蛋、面条(规则)。如果厨师端上来的面没有鸡蛋(违反契约),你可以拒绝付款——契约测试就是这个「检查菜单与实际菜品是否一致」的过程。

1.2 契约测试的核心价值

  • 减少协作成本:前端无需等待后端开发完成,可直接根据契约Mock数据开发;
  • 提前发现问题:避免因API修改(如新增/删除字段)导致消费者崩溃;
  • 明确责任边界:若测试失败,可快速定位是提供者(API不符合契约)还是消费者(调用不符合契约)的问题。

1.3 FastAPI中的契约定位

在FastAPI中,契约不是手动写的——框架会通过「类型注解+Pydantic模型+路径操作」自动生成OpenAPI规范(即/openapi.json),这份规范就是天然的「契约」。这意味着:
你写的API代码=契约定义,无需额外维护两份文档,从根源避免「文档与代码不一致」的问题。

二、OpenAPI规范:FastAPI的「契约DNA」

2.1 FastAPI如何自动生成OpenAPI?

FastAPI的「魔法」在于通过代码自动推导规范。举个简单例子:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str  # 必填字符串
    price: float  # 必填浮点数
    is_offer: bool = None  # 可选布尔值

@app.post("/items/")
def create_item(item: Item):  # 请求体为Item模型
    return {"item_name": item.name, "item_price": item.price}

FastAPI会自动生成以下OpenAPI信息:

  • 路径/items/,方法POST
  • 请求体:需符合Item模型(name必填、price必填、is_offer可选);
  • 响应:返回包含item_nameitem_price的JSON。

你可以通过http://localhost:8000/openapi.json查看完整规范,或通过http://localhost:8000/docs查看可视化文档(Swagger UI)。

2.2 OpenAPI规范的核心要素

一份完整的OpenAPI规范包含以下关键部分(对应FastAPI代码):

规范要素FastAPI实现方式作用
路径(Paths)@app.get("/items/{item_id}")定义API的访问路径和HTTP方法
参数(Parameters)item_id: int = Path(..., ge=1)定义路径/查询/Header参数的约束
请求体(Request Body)item: Item用Pydantic模型定义请求数据结构
响应(Responses)response_model=Item用Pydantic模型定义响应数据结构
模式(Schemas)Pydantic模型(如Item定义数据的类型、约束(如min_length

三、契约测试与OpenAPI的协同实践

3.1 协同逻辑:用OpenAPI做「契约源」

契约测试的核心是「验证API行为符合契约」,而FastAPI的OpenAPI规范就是最准确的契约源。整个流程可总结为:
API代码 → 自动生成OpenAPI契约 → 契约测试工具(如Schemathesis) → 验证API是否符合契约

3.2 工具选择:Schemathesis

Schemathesis是FastAPI生态中最常用的契约测试工具,它能:

  • 自动加载OpenAPI规范;
  • 生成覆盖所有路径、参数、响应的测试用例;
  • 验证请求/响应是否符合契约;
  • 集成到Pytest和CI流程。

3.3 实践步骤:从0到1做契约测试

我们以「用户管理API」为例,完整演示契约测试的流程。

3.3.1 步骤1:编写API代码(生成契约)

首先创建main.py,实现用户的「创建」和「查询」功能:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr, Field
from typing import Optional

# 1. 初始化FastAPI应用(自动生成OpenAPI)
app = FastAPI(title="用户管理API", version="1.0.0")

# 模拟数据库(替换为真实数据库即可)
fake_db = []

# 2. 定义请求/响应模型(Pydantic)
class UserCreate(BaseModel):
    """创建用户的请求模型(消费者需遵守的请求格式)"""
    name: str = Field(..., min_length=2, max_length=50, description="用户名,2-50字符")
    email: EmailStr = Field(..., description="合法邮箱")
    age: Optional[int] = Field(None, ge=0, le=120, description="年龄,0-120岁")

class User(UserCreate):
    """查询用户的响应模型(提供者需遵守的响应格式)"""
    id: int = Field(..., description="用户唯一ID")

# 3. 定义路径操作(自动生成OpenAPI的路径、参数、响应)
@app.post("/users/", response_model=User, status_code=201, summary="创建用户")
def create_user(user_in: UserCreate):
    """创建新用户,返回包含ID的用户信息"""
    # 生成自增ID(模拟数据库操作)
    user_id = len(fake_db) + 1
    # 构造响应数据(严格遵循User模型)
    user = User(id=user_id, **user_in.model_dump())
    fake_db.append(user)
    return user

@app.get("/users/{user_id}", response_model=User, summary="查询用户")
def get_user(user_id: int):
    """根据ID查询用户,未找到返回404"""
    for user in fake_db:
        if user.id == user_id:
            return user
    raise HTTPException(status_code=404, detail="用户未找到")

关键说明

  • response_model=User:强制要求响应数据严格符合User模型(过滤额外字段、保证必填字段存在);
  • model_dump():Pydantic 2.x的方法(替代旧版dict()),确保返回数据与模型一致;
  • 路径参数user_id: int:FastAPI会自动验证类型(若传入字符串,返回422错误)。
3.3.2 步骤2:编写契约测试代码

创建test_contract.py,用Schemathesis和Pytest实现契约测试:

import pytest
from schemathesis import from_asgi
from main import app

# 1. 加载FastAPI的OpenAPI规范(契约源)
# 注意:`/openapi.json`是FastAPI默认的规范路径
schema = from_asgi("/openapi.json", app)

# 2. 编写契约测试用例
@pytest.mark.asyncio  # Schemathesis的call_asgi是异步方法,需加此装饰器
@schema.parametrize()  # 自动生成所有测试用例(覆盖所有路径、参数、响应)
async def test_contract_compliance(case):
    """验证API是否符合OpenAPI契约"""
    # 发送请求到API(用FastAPI的ASGI接口,无需启动服务)
    response = await case.call_asgi(app=app)
    # 验证响应是否符合契约(如字段类型、必填项、响应码)
    case.validate_response(response)
3.3.3 步骤3:运行测试并查看结果

安装依赖(确保版本最新):

pip install fastapi==0.104.1 pydantic==2.5.2 schemathesis==3.17.0 pytest==7.4.3 uvicorn==0.24.0.post1

运行测试:

pytest test_contract.py -v

预期结果

  • 对于/users/的POST请求:验证请求体符合UserCreate、响应符合User
  • 对于/users/{user_id}的GET请求:验证路径参数user_id是整数、响应符合User或404;
  • 若测试通过,输出PASSED;若失败,输出具体错误(如「响应缺少id字段」)。
3.3.4 步骤4:集成CI(持续保障契约一致)

将契约测试集成到GitHub Actions(或GitLab CI),确保每次代码提交都自动运行测试:

# .github/workflows/contract-test.yml
name: 契约测试
on: [push, pull_request]  # 推送或PR时触发
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: 安装Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - name: 安装依赖
        run: pip install -r requirements.txt
      - name: 运行契约测试
        run: pytest test_contract.py -v

四、课后Quiz:巩固核心知识点

问题1:在FastAPI中,契约测试的「契约源」是什么?为什么它比手动写契约更可靠?

答案解析
契约源是FastAPI自动生成的OpenAPI规范
可靠性原因:OpenAPI由API代码推导而来(类型注解、Pydantic模型、路径操作),完全同步代码逻辑——手动写契约容易出现「文档与代码不一致」,而自动生成则避免了这个问题。

问题2:当Schemathesis测试失败,提示「Response schema mismatch: missing required field 'id'」,可能的原因是什么?如何解决?

答案解析

  • 原因:API的响应缺少id字段,违反了User模型的契约(id是必填字段);
  • 解决步骤:
    1. 检查路径操作的response_model是否设置为User(如@app.post("/users/", response_model=User));
    2. 检查返回值是否用User模型构造(如return User(id=user_id, **user_in.model_dump()));
    3. 避免直接返回字典(如return {"name": "张三"}),需用Pydantic模型保证字段完整。

五、常见报错与解决方案

报错1:「422 Validation Error」(请求参数不符合契约)

  • 原因:消费者发送的请求不符合Pydantic模型的约束(如name长度不足2字符、email格式错误);
  • 解决
    1. 检查请求参数是否符合UserCreate模型的定义;
    2. 用FastAPI的/docs调试(输入参数后,文档会提示错误);
    3. 确保消费者使用application/json格式发送请求。

报错2:「Response schema mismatch」(响应不符合契约)

  • 原因:API返回的响应不符合response_model的定义(如缺少id字段、age类型是字符串);
  • 解决
    1. 检查路径操作的response_model是否正确(如User而非UserCreate);
    2. User(**data)构造返回值(而非直接返回字典);
    3. 禁止返回额外字段(如return User(..., extra_field="xxx")会被Pydantic过滤)。

报错3:「OpenAPI schema not found at /openapi.json」

  • 原因:Schemathesis无法加载OpenAPI规范(如FastAPI应用未正确初始化);
  • 解决
    1. 确保app = FastAPI()正确初始化;
    2. 检查from_asgi的路径是否为"/openapi.json"(FastAPI默认路径);
    3. 若用测试客户端,需确保应用处于运行状态(如with TestClient(app) as client:)。

六、流程图:契约测试的完整流程

graph TD
    A[编写API代码<br>(Pydantic模型+路径操作)] --> B[自动生成OpenAPI契约<br>(/openapi.json)]
    B --> C[Schemathesis加载契约<br>生成测试用例]
    C --> D[发送请求到API<br>(验证请求/响应)]
    D --> E{测试结果?}
    E -->|通过| F[API符合契约<br>可交付]
    E -->|失败| G[修正API代码<br>(如调整模型/参数)]
    G --> B[重新生成契约]
    F --> H[集成CI<br>持续保障契约一致]