在 python 利用 FastAPI 建立 API 非常的简单,同样的,要测试它也非常地容易,这里我会使用一个简单的例子,演练 pytest 测试 API (FastAPI)
开发环境
- Windows 11 Home
- PyCharm 2024.3.5
- Python 13.3
- uv 0.6.10
安装
用 uv 建立一个 fastapi 专案
uv init Lab.Py.Memory.ApiTest
cd Lab.Py.Memory.ApiTest
安装相关套件
uv add fastapi>=0.115.12 httpx>=0.28.1 pydantic>=2.11.2 pytest>=8.3.5 python-dateutil>=2.9.0.post0 uvicorn>=0.34.0
专案目录结构
├── app/
│ ├── api/
│ │ ├── __init__.py # API 路由配置
│ │ └── members.py # 会员 API 实现
│ ├── db/
│ │ └── memory_db.py # 记忆体资料库实现
│ ├── models/
│ │ └── member.py # 会员资料模型
│ ├── openapi.yml # OpenAPI 规範文件
│ └── main.py # 应用程式入口
app/db/memory_db.py
MemberRepository 里面用 dict 装载 member 资料,CRUD 处理 dict 物件
from typing import Dict, List, Optional
from app.models.member import Member, MemberCreate, UpdateMemberRequest
from datetime import datetime
class MemberRepository:
def __init__(self):
self.members: Dict[str, Member] = {}
def get_all(self) -> List[Member]:
return list(self.members.values())
def get_by_id(self, member_id: str) -> Optional[Member]:
return self.members.get(member_id)
def create(self, member_create: MemberCreate) -> Member:
dump = member_create.model_dump()
member = Member(**dump)
self.members[member.id] = member
return member
def update(self, member_id: str, update_member_request: UpdateMemberRequest) -> Optional[Member]:
if member_id not in self.members:
return None
current_member = self.members[member_id]
update_data = update_member_request.model_dump(exclude_unset=True)
for key, value in update_data.items():
if value is not None:
setattr(current_member, key, value)
return current_member
def delete(self, member_id: str) -> bool:
if member_id not in self.members:
return False
del self.members[member_id]
return True
# 单例模式,确保整个应用程序中只有一个 MemberDB 实例
member_repository = MemberRepository()
app/models/member.py
会员物件,分别有 first_name、last_name、address、birthday 等资讯
from datetime import date, datetime
from pydantic import BaseModel, Field
from typing import Optional
import uuid
class MemberBase(BaseModel):
first_name: str
last_name: str
age: Optional[int] = None
address: Optional[str] = None
birthday: date
class MemberCreate(MemberBase):
pass
class UpdateMemberRequest(BaseModel):
first_name: Optional[str] = None
last_name: Optional[str] = None
age: Optional[int] = None
address: Optional[str] = None
birthday: Optional[date] = None
class Member(MemberBase):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
created_by: Optional[str] = "system"
created_at: datetime = Field(default_factory=datetime.now)
app/api/members.py
在这档案里的端点都使用这个 router 配置 router = APIRouter(prefix="/members", tags=["members"])
from fastapi import APIRouter, HTTPException, status
from typing import List
from app.models.member import Member, MemberCreate, UpdateMemberRequest
from app.db.memory_db import member_repository
router = APIRouter(prefix="/members", tags=["members"])
@router.get("", response_model=List[Member])
async def get_all_members():
return member_repository.get_all()
@router.post("", response_model=Member, status_code=status.HTTP_201_CREATED)
async def create_member(member_create: MemberCreate):
return member_repository.create(member_create)
@router.get("/{member_id}", response_model=Member)
async def get_member_by_id(member_id: str):
member = member_repository.get_by_id(member_id)
if member is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Member with ID {member_id} not found"
)
return member
@router.put("/{member_id}", response_model=Member)
async def update_member(member_id: str, member_update: UpdateMemberRequest):
member = member_repository.update(member_id, member_update)
if member is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Member with ID {member_id} not found"
)
return member
@router.delete("/{member_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_member(member_id: str):
success = member_repository.delete(member_id)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Member with ID {member_id} not found"
)
app/api/__init__.py
创建了一个新的 APIRouter 实例,并设置了前缀 /api/v1,由于 members_router 有自己的前缀 /members,所以最终的路径会是:
/api/v1/members(获取所有会员、创建新会员)
/api/v1/members/{member_id}(获取、更新、删除特定会员)
from fastapi import APIRouter
from app.api.members import router as members_router
api_router = APIRouter(prefix="/api/v1")
api_router.include_router(members_router)
app/main.py
import uvicorn
from fastapi import FastAPI
from app.api import api_router
import os
app = FastAPI(
title="Member API",
description="RESTful API for managing members with Memory DB",
version="0.1.0"
)
app.include_router(api_router)
@app.get("/")
async def root():
return {"message": "Welcome to Member API. Go to /docs for the API documentation."}
def start():
"""Entry point for the application script"""
uvicorn.run("app.main:app", host="0.0.0.0", port=8001, reload=True)
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8001)
启动 API 服务
按 F5 把 API 架起来
或者用 uvicorn
uvicorn app.main:app --reload --host 0.0.0.0 --port 8001
API 文档
启动应用程式后,可以在以下网址查看 API 文档:
- Swagger UI: http://localhost:8001/docs
- ReDoc: http://localhost:8001/redoc
除了 FastAPI 动态产生的之外,我也在专案结构内写了一份 openapi.yml,看你的团队喜欢用 API First 先写文件,或是先写 Server Code,再产生 API 文件;这两种做法,我比较偏好先写文件,有了文件,就可以产生 Server/Client Code。
API Test
TestClient 很轻易地就可以把 API 架起来,让我们在测试 API 时没有甚么太大的阻力
- 建立被测 API Server:client = TestClient(app)
- 呼叫目标 API Server:response = client.post("/api/v1/members", json=test_member_data)
import pytest
from fastapi.testclient import TestClient
from datetime import date
import uuid
from app.main import app
from app.db.memory_db import member_repository
# 创建测试客户端
client = TestClient(app)
# 在每次测试前清空会员数据库
@pytest.fixture(autouse=True)
def clear_db():
member_repository.members = {}
yield
member_repository.members = {}
# 测试数据
test_member_data = {
"first_name": "张",
"last_name": "三",
"age": 30,
"address": "台北市信义区101号",
"birthday": str(date(1993, 5, 15))
}
def test_create_member():
"""测试创建会员功能"""
response = client.post("/api/v1/members", json=test_member_data)
assert response.status_code == 201
data = response.json()
assert data["first_name"] == test_member_data["first_name"]
assert data["last_name"] == test_member_data["last_name"]
assert data["age"] == test_member_data["age"]
assert data["address"] == test_member_data["address"]
assert data["birthday"] == test_member_data["birthday"]
assert "id" in data
assert "created_at" in data
assert data["created_by"] == "system"
範例程式
https://github.com/yaochangyu/sample.dotblog/tree/master/Test/Lab.Py.Memory.ApiTest
若有谬误,烦请告知,新手发帖请多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET