使用 pytest 测试 API (FastAPI)

在 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

关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章