在 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