目录
- 1. 引言
- 2. 什么是 API
- 3. 搭建前的准备工作
- 4. 第一个 API 接口
- 5. 设计数据模型
- 6. 实现增删改查
- 7. 接口测试与调试
- 8. 部署上线
- 9. API 安全
- 10. 总结与进阶方向
1. 引言
在当今的软件开发中,API(Application Programming Interface,应用程序编程接口)已经成为连接不同系统、服务和数据的关键桥梁。无论是构建 Web 应用、移动端 App,还是实现微服务架构,API 都扮演着不可或缺的角色。
然而,对于许多初学者来说,API 似乎是一个既熟悉又陌生的概念——每天都在使用别人提供的 API,却不知道如何从零开始设计和搭建一个属于自己的 API。本文将从最基础的概念出发,一步步带你完成一个 API 从设计、开发、测试到部署上线的完整流程。
2. 什么是 API
API 本质上是一组定义了软件组件之间如何交互的规则和协议。它允许不同的软件系统之间进行通信和数据交换,而无需了解对方内部的具体实现细节。
2.1 生活中的类比
可以把 API 想象成餐厅里的服务员:
- 你(客户端)向服务员(API)点菜(发送请求)
- 服务员将你的需求传达给厨房(服务器)
- 厨房做好菜后,服务员再将菜品(响应数据)端到你面前
在这个过程中,你不需要知道厨房内部如何运作,只需要按照菜单(API 文档)点菜即可。
2.2 API 的核心组成
一个典型的 Web API 通常包含以下要素:
- 端点(Endpoint):API 的访问地址,如
https://api.example.com/users - 方法(Method):HTTP 动词,如 GET、POST、PUT、DELETE
- 参数(Parameters):请求中携带的数据,可以是路径参数、查询参数或请求体
- 响应(Response):服务器返回的数据,通常为 JSON 或 XML 格式
- 状态码(Status Code):表示请求结果的数字代码,如 200 表示成功、404 表示资源不存在
3. 搭建前的准备工作
在开始编写代码之前,我们需要先做好环境准备和需求分析。
3.1 技术选型
本文以 Python 的 FastAPI 框架为例进行演示,因为它具有以下优势:
- 性能优异,基于 Starlette 和 Pydantic
- 自动生成交互式 API 文档
- 类型提示友好,代码可读性强
- 支持异步编程
当然,你也可以选择其他技术栈,如 Node.js 的 Express、Java 的 Spring Boot 等,核心思路是相通的。
3.2 环境准备
首先,确保你的电脑上安装了 Python 3.8 及以上版本,然后创建虚拟环境并安装依赖:
# 创建项目目录
mkdir my-api
cd my-api
# 创建虚拟环境
python -m venv venv
# 激活虚拟环境(Windows)
venv\Scripts\activate
# 激活虚拟环境(macOS/Linux)
source venv/bin/activate
# 安装 FastAPI 和 Uvicorn
pip install fastapi uvicorn
3.3 需求分析
在动手写代码之前,先明确我们的 API 要解决什么问题。本文以一个简单的「待办事项管理」API 为例,需要实现以下功能:
- 创建待办事项
- 查询待办事项列表
- 查询单个待办事项详情
- 更新待办事项
- 删除待办事项
4. 第一个 API 接口
现在,让我们从最简单的接口开始,感受一下 FastAPI 的开发体验。
4.1 创建主文件
在项目目录下创建 main.py 文件:
from fastapi import FastAPI
# 创建 FastAPI 实例
app = FastAPI(title="待办事项 API", version="1.0.0")
@app.get("/")
def read_root():
return {
"message": "欢迎使用待办事项 API"}
4.2 启动服务
在终端中运行以下命令启动开发服务器:
uvicorn main:app --reload
启动成功后,你会看到类似如下的输出:
INFO: Uvicorn running on http://127.0.0.1:8000
INFO: Application startup complete.
4.3 测试接口
打开浏览器访问 http://127.0.0.1:8000,你会看到返回的 JSON 数据:
{
"message": "欢迎使用待办事项 API"
}
同时,FastAPI 还自动生成了交互式 API 文档,访问 http://127.0.0.1:8000/docs 即可查看和在线调试所有接口。
5. 设计数据模型
接下来,我们使用 Pydantic 定义待办事项的数据模型。Pydantic 是 FastAPI 的数据验证核心,可以自动完成请求数据的类型校验和序列化。
5.1 定义模型
在 main.py 中新增数据模型:
from typing import Optional
from pydantic import BaseModel
from datetime import datetime
class TodoItem(BaseModel):
"""待办事项数据模型"""
id: Optional[int] = None
title: str
description: Optional[str] = None
completed: bool = False
created_at: datetime = datetime.now()
5.2 模型说明
id:待办事项的唯一标识,创建时自动生成title:标题,必填字段description:详细描述,可选字段completed:是否已完成,默认为 Falsecreated_at:创建时间,默认为当前时间
6. 实现增删改查
现在,我们来实现完整的 CRUD(Create、Read、Update、Delete)接口。
6.1 内存数据存储
为了简化演示,我们先使用内存列表存储数据。在实际项目中,你会使用数据库(如 PostgreSQL、MySQL)来持久化数据。
# 内存数据存储
todos = []
todo_id_counter = 1
6.2 创建待办事项
from fastapi import HTTPException
@app.post("/todos", status_code=201)
def create_todo(todo: TodoItem):
"""创建新的待办事项"""
global todo_id_counter
todo.id = todo_id_counter
todo_id_counter += 1
todos.append(todo)
return todo
6.3 查询待办事项列表
@app.get("/todos")
def list_todos(completed: Optional[bool] = None):
"""获取待办事项列表,可按完成状态筛选"""
if completed is None:
return todos
return [todo for todo in todos if todo.completed == completed]
6.4 查询单个待办事项
@app.get("/todos/{todo_id}")
def get_todo(todo_id: int):
"""根据 ID 获取单个待办事项"""
for todo in todos:
if todo.id == todo_id:
return todo
raise HTTPException(status_code=404, detail="待办事项不存在")
6.5 更新待办事项
@app.put("/todos/{todo_id}")
def update_todo(todo_id: int, todo_update: TodoItem):
"""更新指定 ID 的待办事项"""
for index, todo in enumerate(todos):
if todo.id == todo_id:
# 保留原 ID 和创建时间
todo_update.id = todo.id
todo_update.created_at = todo.created_at
todos[index] = todo_update
return todo_update
raise HTTPException(status_code=404, detail="待办事项不存在")
6.6 删除待办事项
@app.delete("/todos/{todo_id}", status_code=204)
def delete_todo(todo_id: int):
"""删除指定 ID 的待办事项"""
for index, todo in enumerate(todos):
if todo.id == todo_id:
todos.pop(index)
return
raise HTTPException(status_code=404, detail="待办事项不存在")
删除成功后返回 204 No Content,表示操作成功且无返回内容。如果待办事项不存在,则返回 404 错误。
6.7 全局异常处理
6.8 数据库集成
前面我们使用内存列表存储数据,服务重启后数据就会丢失。下面我们使用 SQLAlchemy 连接 SQLite 数据库,实现数据的持久化存储。
6.8.1 安装依赖
pip install sqlalchemy
6.8.2 配置数据库
在 main.py 中新增数据库配置和模型:
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime
from sqlalchemy.orm import declarative_base, sessionmaker
from datetime import datetime
# 数据库配置(使用 SQLite,实际项目可替换为 PostgreSQL/MySQL)
DATABASE_URL = "sqlite:///./todos.db"
engine = create_engine(DATABASE_URL, connect_args={
"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
class TodoDB(Base):
"""数据库中的待办事项表"""
__tablename__ = "todos"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, nullable=False)
description = Column(String, nullable=True)
completed = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.now)
# 创建数据表
Base.metadata.create_all(bind=engine)
6.8.3 改造 CRUD 接口
将原来的内存操作替换为数据库操作:
from fastapi import Depends
from sqlalchemy.orm import Session
def get_db():
"""获取数据库会话"""
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.post("/todos", status_code=201)
def create_todo(todo: TodoItem, db: Session = Depends(get_db)):
"""创建新的待办事项(写入数据库)"""
db_todo = TodoDB(
title=todo.title,
description=todo.description,
completed=todo.completed,
)
db.add(db_todo)
db.commit()
db.refresh(db_todo)
return db_todo
@app.get("/todos")
def list_todos(completed: Optional[bool] = None, db: Session = Depends(get_db)):
"""获取待办事项列表(从数据库读取)"""
query = db.query(TodoDB)
if completed is not None:
query = query.filter(TodoDB.completed == completed)
return query.all()
@app.get("/todos/{todo_id}")
def get_todo(todo_id: int, db: Session = Depends(get_db)):
"""根据 ID 获取单个待办事项"""
todo = db.query(TodoDB).filter(TodoDB.id == todo_id).first()
if not todo:
raise HTTPException(status_code=404, detail="待办事项不存在")
return todo
@app.put("/todos/{todo_id}")
def update_todo(todo_id: int, todo_update: TodoItem, db: Session = Depends(get_db)):
"""更新指定 ID 的待办事项"""
todo = db.query(TodoDB).filter(TodoDB.id == todo_id).first()
if not todo:
raise HTTPException(status_code=404, detail="待办事项不存在")
todo.title = todo_update.title
todo.description = todo_update.description
todo.completed = todo_update.completed
db.commit()
db.refresh(todo)
return todo
@app.delete("/todos/{todo_id}", status_code=204)
def delete_todo(todo_id: int, db: Session = Depends(get_db)):
"""删除指定 ID 的待办事项"""
todo = db.query(TodoDB).filter(TodoDB.id == todo_id).first()
if not todo:
raise HTTPException(status_code=404, detail="待办事项不存在")
db.delete(todo)
db.commit()
改造完成后,重启服务,数据就会持久化保存到 todos.db 文件中,即使服务重启数据也不会丢失。
为了让 API 在出错时返回统一、友好的错误信息,我们添加一个全局异常处理器。这样无论是参数校验失败、资源不存在,还是服务器内部错误,客户端都能收到结构一致的 JSON 响应。
from fastapi import Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
"""统一处理 HTTP 异常,返回 JSON 格式的错误信息"""
return JSONResponse(
status_code=exc.status_code,
content={
"code": exc.status_code, "message": exc.detail, "data": None},
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
"""统一处理请求参数校验失败,返回字段级别的错误详情"""
errors = exc.errors()
detail = []
for error in errors:
field = ".".join(str(loc) for loc in error["loc"] if loc != "body")
detail.append({
"field": field, "message": error["msg"]})
return JSONResponse(
status_code=422,
content={
"code": 422, "message": "请求参数校验失败", "data": detail},
)
@app.exception_handler(Exception)
async def unhandled_exception_handler(request: Request, exc: Exception):
"""兜底捕获未处理的异常,避免向客户端泄露堆栈信息"""
return JSONResponse(
status_code=500,
content={
"code": 500, "message": "服务器内部错误,请稍后重试", "data": None},
)
添加后,所有接口的错误响应都会统一为 {"code": ..., "message": ..., "data": ...} 的结构,前端解析起来更加方便。
@app.delete("/todos/{todo_id}", status_code=204)
def delete_todo(todo_id: int):
"""删除指定 ID 的待办事项"""
for index, todo in enumerate(todos):
if todo.id == todo_id:
todos.pop(index)
return
raise HTTPException(status_code=404, detail="待办事项不存在")
7. 接口测试与调试
接口写完后,我们需要进行充分的测试,确保每个接口都能正常工作。除了 Swagger 和 curl,API 还支持多种调用方式,下面逐一介绍。
7.1 使用 Swagger 文档测试
FastAPI 自动生成的 /docs 页面提供了可视化的接口测试工具。你可以:
- 打开
http://127.0.0.1:8000/docs - 点击任意接口展开详情
- 点击「Try it out」按钮
- 填写参数后点击「Execute」发送请求
- 查看响应结果和状态码
7.2 使用 curl 命令测试
在终端中也可以直接使用 curl 进行测试:
# 创建待办事项
curl -X POST "http://127.0.0.1:8000/todos" \
-H "Content-Type: application/json" \
-d '{"title": "学习 FastAPI", "description": "完成 API 开发教程"}'
# 查询列表
curl "http://127.0.0.1:8000/todos"
# 查询单个
curl "http://127.0.0.1:8000/todos/1"
# 更新
curl -X PUT "http://127.0.0.1:8000/todos/1" \
-H "Content-Type: application/json" \
-d '{"title": "学习 FastAPI", "description": "已完成", "completed": true}'
# 删除
curl -X DELETE "http://127.0.0.1:8000/todos/1"
7.3 使用 Python requests 调用
在 Python 脚本或 Jupyter Notebook 中,可以使用 requests 库调用 API:
import requests
BASE_URL = "http://127.0.0.1:8000"
# 创建待办事项
resp = requests.post(
f"{
BASE_URL}/todos",
json={
"title": "学习 FastAPI", "description": "完成 API 开发教程"},
)
print(resp.status_code, resp.json())
# 查询列表
resp = requests.get(f"{
BASE_URL}/todos")
print(resp.status_code, resp.json())
# 查询单个
resp = requests.get(f"{
BASE_URL}/todos/1")
print(resp.status_code, resp.json())
# 更新
resp = requests.put(
f"{
BASE_URL}/todos/1",
json={
"title": "学习 FastAPI", "description": "已完成", "completed": True},
)
print(resp.status_code, resp.json())
# 删除
resp = requests.delete(f"{
BASE_URL}/todos/1")
print(resp.status_code)
7.4 使用 JavaScript fetch 调用
在前端浏览器或 Node.js 环境中,可以使用 fetch 调用 API:
const BASE_URL = "http://127.0.0.1:8000";
// 创建待办事项
const createResp = await fetch(`${
BASE_URL}/todos`, {
method: "POST",
headers: {
"Content-Type": "application/json" },
body: JSON.stringify({
title: "学习 FastAPI",
description: "完成 API 开发教程",
}),
});
console.log(createResp.status, await createResp.json());
// 查询列表
const listResp = await fetch(`${
BASE_URL}/todos`);
console.log(listResp.status, await listResp.json());
// 查询单个
const getResp = await fetch(`${
BASE_URL}/todos/1`);
console.log(getResp.status, await getResp.json());
// 更新
const updateResp = await fetch(`${
BASE_URL}/todos/1`, {
method: "PUT",
headers: {
"Content-Type": "application/json" },
body: JSON.stringify({
title: "学习 FastAPI",
description: "已完成",
completed: true,
}),
});
console.log(updateResp.status, await updateResp.json());
// 删除
const deleteResp = await fetch(`${
BASE_URL}/todos/1`, {
method: "DELETE" });
console.log(deleteResp.status);
7.5 使用 Postman / Apifox 图形化工具
Postman 和 Apifox 是流行的图形化 API 调试工具,适合团队协作和接口文档管理:
- 新建请求,选择方法(GET/POST/PUT/DELETE)
- 填写 URL,如
http://127.0.0.1:8000/todos - 在 Body 选项卡选择
raw+JSON,填入请求体 - 点击 Send 发送请求,查看响应结果
- 可将接口保存到集合中,方便复用和分享
调用方式选择建议:日常调试用 Swagger 或 Postman;脚本自动化用 Python
requests;前端联调用fetch或axios;命令行快速验证用 curl。
接口写完后,我们需要进行充分的测试,确保每个接口都能正常工作。
7.1 使用 Swagger 文档测试
FastAPI 自动生成的 /docs 页面提供了可视化的接口测试工具。你可以:
- 打开
http://127.0.0.1:8000/docs - 点击任意接口展开详情
- 点击「Try it out」按钮
- 填写参数后点击「Execute」发送请求
- 查看响应结果和状态码
7.2 使用 curl 命令测试
在终端中也可以直接使用 curl 进行测试:
# 创建待办事项
curl -X POST "http://127.0.0.1:8000/todos" \
-H "Content-Type: application/json" \
-d '{"title": "学习 FastAPI", "description": "完成 API 开发教程"}'
# 查询列表
curl "http://127.0.0.1:8000/todos"
# 查询单个
curl "http://127.0.0.1:8000/todos/1"
# 更新
curl -X PUT "http://127.0.0.1:8000/todos/1" \
-H "Content-Type: application/json" \
-d '{"title": "学习 FastAPI", "description": "已完成", "completed": true}'
# 删除
curl -X DELETE "http://127.0.0.1:8000/todos/1"
7.3 常见问题排查
在开发和测试过程中,你可能会遇到一些常见问题。下面按错误类型给出排查思路和解决方案。
7.3.1 404 错误:资源不存在
现象:请求返回 404 Not Found。
可能原因与解决方案:
- URL 路径写错:检查路径是否与路由定义完全一致,注意大小写和末尾斜杠
- 资源 ID 不存在:确认查询的 ID 是否在数据库中存在,可先调用列表接口确认
- 路由顺序问题:如果定义了
/todos/{todo_id}和/todos/me这类路由,注意静态路径要放在动态路径之前
7.3.2 422 错误:参数校验失败
现象:请求返回 422 Unprocessable Entity,响应体中包含字段级别的错误详情。
可能原因与解决方案:
- 缺少必填字段:如创建待办事项时未传
title,检查请求体是否完整 - 字段类型错误:如
completed传了字符串"true"而不是布尔值true,检查 JSON 中的类型 - 路径参数类型错误:如
/todos/abc中abc无法转换为int,确认路径参数类型
7.3.3 500 错误:服务器内部异常
现象:请求返回 500 Internal Server Error。
可能原因与解决方案:
- 数据库连接失败:检查数据库服务是否启动、连接串是否正确
- 代码逻辑异常:查看终端日志中的堆栈信息,定位具体报错行
- 依赖缺失:确认所有依赖都已安装,可执行
pip list检查
7.3.4 服务无法启动
现象:运行 uvicorn main:app --reload 时报错退出。
可能原因与解决方案:
- 端口被占用:换一个端口启动,如
uvicorn main:app --port 8001 - 导入错误:检查
main.py中是否有语法错误或未安装的依赖 - 虚拟环境未激活:确认当前终端使用的是项目虚拟环境
7.3.5 数据不持久化
现象:服务重启后,之前创建的数据丢失。
可能原因与解决方案:
- 仍在使用内存存储:确认是否已按第 6.8 节改造为数据库存储
- 数据库文件路径问题:检查
DATABASE_URL中的路径是否正确,SQLite 文件是否生成 - 未执行建表:确认
Base.metadata.create_all(bind=engine)已执行
排查技巧:遇到问题时,先看终端日志中的堆栈信息,再结合响应体中的
code和message字段定位问题,通常能快速找到根因。
7.4 接口性能对比
为了直观展示不同实现方式的性能差异,下面用一张对比图说明内存存储与数据库存储、同步与异步接口、有无缓存等场景下的响应耗时情况。
从图中可以看出:
- 内存存储:响应最快,但数据无法持久化,适合缓存或临时数据
- SQLite 存储:本地磁盘 I/O,速度尚可,适合中小型应用和开发环境
- PostgreSQL 存储:网络 I/O 开销较大,但支持并发、事务和水平扩展,适合生产环境
- SQLite + Redis 缓存:缓存命中时响应接近内存速度,适合高频读取、低频更新的数据
- 异步 PostgreSQL:通过异步驱动减少线程阻塞,在高并发下吞吐量显著提升
读多写少场景:优先考虑加缓存;写多读少场景:优先考虑异步驱动和连接池;高并发场景:两者结合效果最佳。
7.4.1 性能对比数据表
为了更直观地对比不同实现方式的性能差异,下面给出一个参考数据表(基于 1000 次请求的平均值):
| 实现方式 | 平均响应耗时 | 吞吐量(QPS) | 缓存命中率 | 数据持久化 | 适用场景 |
|---|---|---|---|---|---|
| 内存存储 | 0.1ms - 0.5ms | 5000+ | 100%(无缓存) | ❌ | 缓存、临时数据 |
| SQLite 存储 | 1ms - 5ms | 800 - 1500 | 0%(无缓存) | ✅ | 中小型应用、开发环境 |
| PostgreSQL 存储 | 5ms - 20ms | 300 - 800 | 0%(无缓存) | ✅ | 生产环境、高并发 |
| SQLite + Redis 缓存 | 0.2ms - 1ms | 3000+ | 90% - 99% | ✅ | 高频读取、低频更新 |
| 异步 PostgreSQL | 3ms - 10ms | 1000 - 3000 | 0%(无缓存) | ✅ | 高并发写入、大数据量 |
缓存命中率说明:缓存命中率越高,实际响应越接近内存速度。当缓存命中率低于 80% 时,建议检查缓存淘汰策略或调整过期时间,避免缓存穿透和雪崩。
7.5 实战测试:完整流程演练
下面通过一个完整的实战演练,把前面学到的接口串起来跑一遍,验证 CRUD 全流程的正确性。
7.5.1 准备测试数据
先创建两条待办事项作为测试数据:
# 创建第一条待办事项
curl -X POST "http://127.0.0.1:8000/todos" \
-H "Content-Type: application/json" \
-d '{"title": "学习 FastAPI", "description": "完成 API 开发教程"}'
# 创建第二条待办事项
curl -X POST "http://127.0.0.1:8000/todos" \
-H "Content-Type: application/json" \
-d '{"title": "编写单元测试", "description": "为 CRUD 接口补充测试用例"}'
7.5.2 验证查询功能
# 查询列表,应返回 2 条记录
curl "http://127.0.0.1:8000/todos"
# 按完成状态筛选
curl "http://127.0.0.1:8000/todos?completed=false"
# 查询单个
curl "http://127.0.0.1:8000/todos/1"
7.5.3 验证更新功能
# 将第一条待办事项标记为已完成
curl -X PUT "http://127.0.0.1:8000/todos/1" \
-H "Content-Type: application/json" \
-d '{"title": "学习 FastAPI", "description": "完成 API 开发教程", "completed": true}'
7.5.4 验证删除功能
# 删除第二条待办事项
curl -X DELETE "http://127.0.0.1:8000/todos/2"
# 再次查询列表,应只剩 1 条记录
curl "http://127.0.0.1:8000/todos"
7.5.5 验证错误处理
# 查询不存在的 ID,应返回 404
curl "http://127.0.0.1:8000/todos/999"
# 缺少必填字段,应返回 422
curl -X POST "http://127.0.0.1:8000/todos" \
-H "Content-Type: application/json" \
-d '{"description": "缺少 title 字段"}'
7.5.6 使用 Python 脚本自动化测试
手动测试只能验证单个接口,下面用 Python 脚本把整个流程自动化跑一遍,并断言每个步骤的返回结果:
import requests
BASE_URL = "http://127.0.0.1:8000"
def test_create_todo():
"""测试创建待办事项"""
resp = requests.post(
f"{
BASE_URL}/todos",
json={
"title": "学习 FastAPI", "description": "完成 API 开发教程"},
)
assert resp.status_code == 201, f"创建失败: {
resp.status_code}"
data = resp.json()
assert data["title"] == "学习 FastAPI"
print(f"✅ 创建成功: id={
data['id']}")
return data["id"]
def test_list_todos():
"""测试查询列表"""
resp = requests.get(f"{
BASE_URL}/todos")
assert resp.status_code == 200
assert isinstance(resp.json(), list)
print(f"✅ 查询列表成功: 共 {
len(resp.json())} 条")
def test_get_todo(todo_id: int):
"""测试查询单个"""
resp = requests.get(f"{
BASE_URL}/todos/{
todo_id}")
assert resp.status_code == 200
assert resp.json()["id"] == todo_id
print(f"✅ 查询单个成功: id={
todo_id}")
def test_update_todo(todo_id: int):
"""测试更新"""
resp = requests.put(
f"{
BASE_URL}/todos/{
todo_id}",
json={
"title": "学习 FastAPI", "description": "已完成", "completed": True},
)
assert resp.status_code == 200
assert resp.json()["completed"] is True
print(f"✅ 更新成功: id={
todo_id}")
def test_delete_todo(todo_id: int):
"""测试删除"""
resp = requests.delete(f"{
BASE_URL}/todos/{
todo_id}")
assert resp.status_code == 204
print(f"✅ 删除成功: id={
todo_id}")
def test_not_found():
"""测试 404 错误处理"""
resp = requests.get(f"{
BASE_URL}/todos/99999")
assert resp.status_code == 404
assert resp.json()["code"] == 404
print("✅ 404 错误处理正确")
def test_validation_error():
"""测试 422 参数校验错误"""
resp = requests.post(f"{
BASE_URL}/todos", json={
"description": "缺少 title"})
assert resp.status_code == 422
assert resp.json()["code"] == 422
print("✅ 422 参数校验错误处理正确")
if __name__ == "__main__":
print("开始自动化测试...\n")
todo_id = test_create_todo()
test_list_todos()
test_get_todo(todo_id)
test_update_todo(todo_id)
test_delete_todo(todo_id)
test_not_found()
test_validation_error()
print("\n🎉 全部测试通过!")
运行脚本:
python test_api.py
预期输出:
开始自动化测试...
✅ 创建成功: id=1
✅ 查询列表成功: 共 1 条
✅ 查询单个成功: id=1
✅ 更新成功: id=1
✅ 删除成功: id=1
✅ 404 错误处理正确
✅ 422 参数校验错误处理正确
🎉 全部测试通过!
测试要点:自动化测试脚本应覆盖正常流程(增删改查)和异常流程(404、422),并断言状态码和关键字段。实际项目中可结合
pytest编写更完整的测试套件,并接入 CI/CD 流水线。
7.5.7 使用 pytest 编写自动化测试
除了手动调用脚本,更推荐使用 pytest 编写可重复执行的自动化测试。这样每次修改代码后,只需运行一条命令即可验证所有接口是否正常。
首先安装 pytest 和 httpx(FastAPI 的测试客户端依赖):
pip install pytest httpx
然后创建 test_main.py 测试文件:
import pytest
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_create_todo():
"""测试创建待办事项"""
resp = client.post("/todos", json={
"title": "学习 pytest",
"description": "编写自动化测试",
})
assert resp.status_code == 201
data = resp.json()
assert data["title"] == "学习 pytest"
assert data["completed"] is False
assert "id" in data
def test_list_todos():
"""测试查询待办事项列表"""
# 先创建一条数据
client.post("/todos", json={
"title": "测试列表"})
resp = client.get("/todos")
assert resp.status_code == 200
assert isinstance(resp.json(), list)
assert len(resp.json()) >= 1
def test_get_todo_not_found():
"""测试查询不存在的待办事项返回 404"""
resp = client.get("/todos/99999")
assert resp.status_code == 404
assert resp.json()["code"] == 404
def test_update_todo():
"""测试更新待办事项"""
# 先创建一条数据
create_resp = client.post("/todos", json={
"title": "待更新"})
todo_id = create_resp.json()["id"]
resp = client.put(f"/todos/{
todo_id}", json={
"title": "已更新",
"completed": True,
})
assert resp.status_code == 200
assert resp.json()["title"] == "已更新"
assert resp.json()["completed"] is True
def test_delete_todo():
"""测试删除待办事项"""
# 先创建一条数据
create_resp = client.post("/todos", json={
"title": "待删除"})
todo_id = create_resp.json()["id"]
resp = client.delete(f"/todos/{
todo_id}")
assert resp.status_code == 204
# 再次查询应返回 404
resp = client.get(f"/todos/{
todo_id}")
assert resp.status_code == 404
def test_validation_error():
"""测试参数校验失败返回 422"""
resp = client.post("/todos", json={
"title": ""})
assert resp.status_code == 422
assert resp.json()["code"] == 422
运行测试:
pytest test_main.py -v
输出示例:
============================= test session starts =============================
collected 6 items
test_main.py::test_create_todo PASSED
test_main.py::test_list_todos PASSED
test_main.py::test_get_todo_not_found PASSED
test_main.py::test_update_todo PASSED
test_main.py::test_delete_todo PASSED
test_main.py::test_validation_error PASSED
============================== 6 passed in 0.35s ==============================
测试要点:使用
TestClient无需启动真实服务器即可模拟请求;每个测试用例相互独立,建议在测试前清空数据库或使用独立测试库;断言时同时检查状态码和响应体,确保接口行为符合预期。
7.5.8 使用 pytest 测试数据库集成
在完成数据库改造后,我们需要验证 CRUD 接口与 SQLite 的集成是否正常。下面是一个针对数据库版本的 pytest 测试脚本:
# test_db_integration.py
import os
import tempfile
import pytest
from fastapi.testclient import TestClient
# 使用临时数据库,避免污染开发数据
os.environ["DATABASE_URL"] = f"sqlite:///{
tempfile.mkdtemp()}/test.db"
from main import app, Base, engine, SessionLocal, TodoDB
# 创建测试表
Base.metadata.create_all(bind=engine)
client = TestClient(app)
@pytest.fixture(autouse=True)
def clean_db():
"""每个测试用例前清空数据库"""
db = SessionLocal()
db.query(TodoDB).delete()
db.commit()
db.close()
yield
def test_create_todo_persists_to_db():
"""创建待办事项后,数据应写入数据库"""
resp = client.post("/todos", json={
"title": "数据库测试", "description": "验证持久化"})
assert resp.status_code == 201
data = resp.json()
assert data["title"] == "数据库测试"
# 直接从数据库验证
db = SessionLocal()
todo = db.query(TodoDB).filter(TodoDB.id == data["id"]).first()
assert todo is not None
assert todo.title == "数据库测试"
db.close()
def test_list_todos_reads_from_db():
"""列表接口应返回数据库中的数据"""
# 先创建一条数据
client.post("/todos", json={
"title": "待查询事项"})
resp = client.get("/todos")
assert resp.status_code == 200
assert len(resp.json()) == 1
assert resp.json()[0]["title"] == "待查询事项"
def test_update_todo_syncs_to_db():
"""更新接口应同步修改数据库记录"""
create_resp = client.post("/todos", json={
"title": "原始标题"})
todo_id = create_resp.json()["id"]
update_resp = client.put(
f"/todos/{
todo_id}",
json={
"title": "更新后的标题", "completed": True},
)
assert update_resp.status_code == 200
assert update_resp.json()["title"] == "更新后的标题"
# 验证数据库中的记录已更新
db = SessionLocal()
todo = db.query(TodoDB).filter(TodoDB.id == todo_id).first()
assert todo.title == "更新后的标题"
assert todo.completed is True
db.close()
def test_delete_todo_removes_from_db():
"""删除接口应移除数据库记录"""
create_resp = client.post("/todos", json={
"title": "待删除事项"})
todo_id = create_resp.json()["id"]
delete_resp = client.delete(f"/todos/{
todo_id}")
assert delete_resp.status_code == 204
# 验证数据库中的记录已删除
db = SessionLocal()
todo = db.query(TodoDB).filter(TodoDB.id == todo_id).first()
assert todo is None
db.close()
运行测试:
pytest test_db_integration.py -v
测试要点:使用
TestClient模拟真实 HTTP 请求,无需启动服务器;通过autousefixture 在每个用例前清空数据库,保证测试隔离;测试结束后临时数据库自动清理,不影响开发数据。
7.5.9 使用 pytest 测试部署后的接口
部署完成后,我们同样可以使用 pytest 对线上接口进行冒烟测试,确保服务在云平台上正常运行。下面是一个针对部署环境的测试脚本:
# test_deployed_api.py
"""
部署环境冒烟测试
验证线上 API 的核心功能是否正常
"""
import os
import requests
import pytest
# 从环境变量读取线上地址,本地测试时使用默认值
BASE_URL = os.getenv("DEPLOYED_API_URL", "https://your-app.railway.app")
@pytest.fixture(scope="module")
def base_url():
"""返回线上 API 地址"""
return BASE_URL
def test_health_check(base_url):
"""健康检查接口应返回 200"""
resp = requests.get(f"{
base_url}/health", timeout=10)
assert resp.status_code == 200
data = resp.json()
assert data["status"] == "ok"
def test_create_todo(base_url):
"""线上创建待办事项"""
resp = requests.post(
f"{
base_url}/todos",
json={
"title": "部署冒烟测试", "description": "验证线上创建功能"},
timeout=10,
)
assert resp.status_code == 201
data = resp.json()
assert data["title"] == "部署冒烟测试"
return data["id"]
def test_list_todos(base_url):
"""线上查询列表"""
resp = requests.get(f"{
base_url}/todos", timeout=10)
assert resp.status_code == 200
assert isinstance(resp.json(), list)
def test_get_todo(base_url):
"""线上查询单个待办事项"""
# 先创建一个
create_resp = requests.post(
f"{
base_url}/todos",
json={
"title": "查询测试"},
timeout=10,
)
todo_id = create_resp.json()["id"]
resp = requests.get(f"{
base_url}/todos/{
todo_id}", timeout=10)
assert resp.status_code == 200
assert resp.json()["id"] == todo_id
def test_update_todo(base_url):
"""线上更新待办事项"""
create_resp = requests.post(
f"{
base_url}/todos",
json={
"title": "更新前", "completed": False},
timeout=10,
)
todo_id = create_resp.json()["id"]
resp = requests.put(
f"{
base_url}/todos/{
todo_id}",
json={
"title": "更新后", "completed": True},
timeout=10,
)
assert resp.status_code == 200
data = resp.json()
assert data["title"] == "更新后"
assert data["completed"] is True
def test_delete_todo(base_url):
"""线上删除待办事项"""
create_resp = requests.post(
f"{
base_url}/todos",
json={
"title": "待删除"},
timeout=10,
)
todo_id = create_resp.json()["id"]
resp = requests.delete(f"{
base_url}/todos/{
todo_id}", timeout=10)
assert resp.status_code == 204
def test_404_for_missing_todo(base_url):
"""查询不存在的待办事项应返回 404"""
resp = requests.get(f"{
base_url}/todos/999999", timeout=10)
assert resp.status_code == 404
data = resp.json()
assert data["code"] == 404
运行方式:
# 本地测试
pytest test_deployed_api.py -v
# 指定线上地址测试
DEPLOYED_API_URL="https://your-app.railway.app" pytest test_deployed_api.py -v
部署测试要点:冒烟测试覆盖健康检查与核心 CRUD 流程,确保线上服务可用;通过环境变量注入线上地址,避免把测试地址硬编码;建议在 CI/CD 流水线中部署完成后自动执行冒烟测试,快速发现回归问题。
7.5.10 使用 curl 脚本进行端到端冒烟测试
除了使用 pytest 编写单元测试,我们还可以编写一个轻量级的 shell 脚本,对部署后的 API 进行快速的端到端冒烟测试。这种方式非常适合在 CI/CD 流水线中作为部署后的健康检查步骤。
#!/usr/bin/env bash
# smoke_test.sh - API 冒烟测试脚本
# 用法: ./smoke_test.sh [BASE_URL]
# 示例: ./smoke_test.sh http://127.0.0.1:8000
set -e # 任何命令失败则立即退出
BASE_URL="${1:-http://127.0.0.1:8000}"
PASS=0
FAIL=0
# 辅助函数:检查 HTTP 状态码
check_status() {
local expected="$1"
local actual="$2"
local description="$3"
if [ "$expected" -eq "$actual" ]; then
echo "✅ PASS: $description (HTTP $actual)"
PASS=$((PASS + 1))
else
echo "❌ FAIL: $description (期望 HTTP $expected, 实际 HTTP $actual)"
FAIL=$((FAIL + 1))
fi
}
echo "========================================"
echo "开始 API 冒烟测试: $BASE_URL"
echo "========================================"
# 1. 健康检查
echo ""
echo "--- 1. 健康检查 ---"
HEALTH_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/health")
check_status 200 "$HEALTH_CODE" "健康检查接口"
# 2. 创建待办事项
echo ""
echo "--- 2. 创建待办事项 ---"
CREATE_RESP=$(curl -s -X POST "$BASE_URL/todos" \
-H "Content-Type: application/json" \
-d '{"title": "冒烟测试任务", "description": "由脚本自动创建"}')
CREATE_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE_URL/todos" \
-H "Content-Type: application/json" \
-d '{"title": "冒烟测试任务", "description": "由脚本自动创建"}')
check_status 201 "$CREATE_CODE" "创建待办事项"
# 提取新创建的 ID(使用 Python 解析 JSON)
TODO_ID=$(echo "$CREATE_RESP" | python3 -c "import sys, json; print(json.load(sys.stdin)['id'])" 2>/dev/null || echo "1")
echo " 创建成功,新 ID = $TODO_ID"
# 3. 查询列表
echo ""
echo "--- 3. 查询待办事项列表 ---"
LIST_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/todos")
check_status 200 "$LIST_CODE" "查询待办事项列表"
# 4. 查询单个
echo ""
echo "--- 4. 查询单个待办事项 ---"
GET_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/todos/$TODO_ID")
check_status 200 "$GET_CODE" "查询单个待办事项 (ID=$TODO_ID)"
# 5. 更新待办事项
echo ""
echo "--- 5. 更新待办事项 ---"
UPDATE_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X PUT "$BASE_URL/todos/$TODO_ID" \
-H "Content-Type: application/json" \
-d '{"title": "冒烟测试任务(已更新)", "completed": true}')
check_status 200 "$UPDATE_CODE" "更新待办事项 (ID=$TODO_ID)"
# 6. 删除待办事项
echo ""
echo "--- 6. 删除待办事项 ---"
DELETE_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE "$BASE_URL/todos/$TODO_ID")
check_status 204 "$DELETE_CODE" "删除待办事项 (ID=$TODO_ID)"
# 7. 验证删除后返回 404
echo ""
echo "--- 7. 验证删除后返回 404 ---"
NOT_FOUND_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/todos/$TODO_ID")
check_status 404 "$NOT_FOUND_CODE" "删除后查询应返回 404"
echo ""
echo "========================================"
echo "测试完成: 通过 $PASS 项, 失败 $FAIL 项"
echo "========================================"
# 如果有失败项,以非零状态码退出(供 CI 判断)
if [ "$FAIL" -gt 0 ]; then
exit 1
fi
运行方式:
# 给脚本添加执行权限
chmod +x smoke_test.sh
# 本地测试
./smoke_test.sh http://127.0.0.1:8000
# 测试部署后的线上环境
./smoke_test.sh https://my-api.onrender.com
冒烟测试要点:脚本覆盖了 CRUD 全流程和健康检查,任何一步失败都会以非零状态码退出;
set -e确保中间步骤失败时立即终止,避免误报;建议将该脚本接入 CI/CD 流水线,在每次部署后自动执行,快速发现回归问题。
7.5.11 使用 pytest 测试完整 CRUD 流程
除了针对单个接口的测试,我们还可以编写一个覆盖完整 CRUD 流程的端到端测试,验证「创建 → 查询 → 更新 → 删除」全链路是否正常。下面给出一个完整的测试脚本:
# test_crud_flow.py
"""
完整 CRUD 流程测试
覆盖创建、查询、更新、删除的端到端验证
"""
import requests
import pytest
BASE_URL = "http://127.0.0.1:8000"
@pytest.fixture
381



被折叠的 条评论
为什么被折叠?



