Stories

Detail Return Return

為什麼 TDD 能讓你的 FastAPI 開發飛起來? - Stories Detail


url: /posts/c9c1e3bb0fdc15303b9b3b1f20124b0b/
title: 為什麼TDD能讓你的FastAPI開發飛起來?
date: 2025-09-12T01:47:01+08:00
lastmod: 2025-09-12T01:47:01+08:00
author: cmdragon
cover: /images/4b9f354d7e634f868f6e429b139ba441~tplv-5jbd59dj06-image.png

summary:
TDD(測試驅動開發)是一種先寫測試再寫代碼的開發模式,遵循“紅-綠-重構”循環。在FastAPI中使用TDD有助於接口契約優先、防止迴歸錯誤、加速開發和生成文檔。通過創建測試環境、編寫測試文件和實現業務代碼,可以逐步驗證功能。例如,用户註冊接口需驗證郵箱格式、密碼強度和重複郵箱,返回201狀態碼及用户ID。TDD流程包括編寫失敗測試、實現通過代碼、測試變綠和重構優化,最終通過pytest運行測試驗證功能。

categories:

  • fastapi

tags:

  • TDD
  • FastAPI
  • 測試驅動開發
  • 紅綠重構循環
  • 接口契約優先
  • 防止迴歸錯誤
  • 測試即文檔

<img src="https://api2.cmdragon.cn/upload/cmder/20250304_012821924.jpg" title="cmdragon_cn.png" alt="cmdragon_cn.png"/>

掃描二維碼
關注或者微信搜一搜:編程智域 前端至全棧交流與成長

發現1000+提升效率與開發的AI工具和實用程序:https://tools.cmdragon.cn/

1. 迭代式接口開發驗證流程的核心邏輯

1.1 什麼是“迭代式”?

迭代式開發不是“一次性寫完所有功能”,而是把接口拆成多個小週期
:每個週期只解決一個具體問題(比如“實現用户創建”→“加密碼哈希”→“加郵箱唯一性校驗”),每個週期都遵循“定義契約→寫測試→實現功能→重構”的閉環。這種方式的好處是
風險可控——每一步都能驗證功能正確性,避免最後發現“全盤錯誤”的情況。

1.2 FastAPI下的TDD適配:從接口契約到測試

FastAPI的類型提示Pydantic模型天然支持TDD的“契約優先”原則。接口的“契約”就是請求/響應的數據格式
(比如“創建用户需要傳哪些字段?返回哪些字段?”),而Pydantic模型就是這個契約的“文字版”。測試則是“驗證契約是否被遵守”——比如測試接口是否返回契約規定的字段,是否拒絕不符合契約的請求。

2. 第一步:定義接口契約(紅)

TDD的第一步是“寫失敗的測試”,但在FastAPI中,我們需要先明確接口的“契約”——用Pydantic定義請求和響應模型。這一步的核心是:*
先和前端/客户端約定“數據格式”,再寫測試驗證這個約定是否被遵守*。

2.1 用Pydantic定義契約模型

比如,我們要做一個“用户創建接口”,需要接收用户名、郵箱、密碼,返回用户ID、用户名、郵箱(不返回密碼)。用Pydantic定義如下:

# models.py(接口契約文件)
from pydantic import BaseModel, EmailStr, Field


# 請求模型:客户端需要傳的參數
class UserCreate(BaseModel):
    username: str = Field(..., min_length=3, max_length=50)  # 必須,3-50字符
    email: EmailStr  # 必須是合法郵箱格式
    password: str = Field(..., min_length=8)  # 必須,至少8位


# 響應模型:服務器返回的結果
class UserOut(BaseModel):
    id: int
    username: str
    email: EmailStr

    class Config:
        orm_mode = True  # 後續對接ORM(如SQLAlchemy)時用

2.2 編寫第一個失敗的測試

契約定義好後,我們寫測試驗證接口是否存在並遵守契約。此時接口還沒實現,測試會失敗(紅階段)。

# test_users.py(測試文件)
from fastapi.testclient import TestClient
from main import app  # 先創建空的main.py,後續填充

client = TestClient(app)


def test_create_user_success():
    # 1. 構造符合契約的請求數據
    user_data = {
        "username": "testuser",
        "email": "test@example.com",
        "password": "testpass123"
    }
    # 2. 發送請求(此時接口未實現,會返回404)
    response = client.post("users", json=user_data)
    # 3. 斷言:期望接口返回201(創建成功),但實際返回404,測試失敗
    assert response.status_code == 201
    # 4. 斷言:響應數據符合UserOut模型(比如沒有password字段)
    response_json = response.json()
    assert "password" not in response_json
    assert response_json["username"] == "testuser"

3. 第二步:實現最小可用接口(綠)

紅階段的測試失敗後,我們需要寫最少的代碼讓測試通過——這就是“最小可用接口”。核心原則是:**只實現契約規定的功能,不做額外擴展
**。

3.1 編寫FastAPI路由

main.py中寫路由,直接返回符合契約的響應:

# main.py
from fastapi import FastAPI
from models import UserCreate, UserOut  # 導入契約模型

app = FastAPI()


# 路由:POST /users/,響應模型是UserOut(遵守契約)
@app.post(

    "users", response_model=UserOut, status_code=201)
def create_user(user_data: UserCreate):  # 自動校驗請求數據是否符合UserCreate
    # 最小實現:固定ID為1,忽略密碼(後續迭代再優化)
    return UserOut(
        id=1,
        username=user_data.username,
        email=user_data.email
    )

3.2 讓測試通過的關鍵:匹配契約

此時重新運行pytest test_users.pytest_create_user_success通過(綠階段)——因為:

  • 接口/users/存在了(返回201);
  • 響應數據符合UserOut模型(沒有password字段,usernameemail正確)。

4. 第三步:重構與擴展(藍)

綠階段的代碼能“用”但不一定“好”,比如上面的create_user直接把業務邏輯寫在路由裏,不利於維護。重構的目標是*
優化代碼結構,但不改變接口的外部行為*(測試仍然通過)。

4.1 分離業務邏輯(重構)

把用户創建的業務邏輯抽到crud.py中,讓路由只負責“接收請求→調用業務邏輯→返回響應”:

# crud.py(業務邏輯文件)
from models import UserCreate, UserOut


def create_user(user_in: UserCreate) -> UserOut:
    # 這裏可以後續加密碼哈希、數據庫操作等邏輯
    return UserOut(
        id=1,
        username=user_in.username,
        email=user_in.email
    )

修改main.py的路由:

# main.py(重構後)
from fastapi import FastAPI
from models import UserCreate, UserOut
from crud import create_user  # 導入業務邏輯

app = FastAPI()


@app.post(

    "users", response_model=UserOut, status_code=201)
def create_user_route(user_data: UserCreate):
    return create_user(user_data)  # 調用業務邏輯

此時運行測試,仍然通過——因為接口的輸入輸出沒有變,只是內部結構更清晰了。

4.2 擴展:新增密碼哈希功能(迭代)

接下來我們要加“密碼哈希”的需求,這時候需要新增測試→修改代碼→保持測試通過

  1. 新增測試:驗證密碼不是明文存儲(用SQLAlchemy做數據庫):

    # test_users.py(新增測試)
    from sqlalchemy import create_engine
    from sqlalchemy.orm import sessionmaker
    from models import Base, UserDB  # 新增UserDB數據庫模型
    
    # 測試用數據庫(內存SQLite)
    engine = create_engine("sqlite://:memory:")
    TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
    Base.metadata.create_all(bind=engine)  # 創建表
    
    def test_password_hashed():
        db = TestingSessionLocal()
        user = db.query(UserDB).filter(UserDB.username == "testuser").first()
        assert user.hashed_password != "testpass123"  # 密碼不是明文
        db.close()
  2. 修改業務邏輯:用passlib哈希密碼:

    # crud.py(修改後)
    from passlib.context import CryptContext
    from sqlalchemy.orm import Session
    from models import UserCreate, UserOut, UserDB
    
    pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
    
    def get_password_hash(password: str) -> str:
        return pwd_context.hash(password)
    
    def create_user(db: Session, user_in: UserCreate) -> UserOut:
        # 哈希密碼
        hashed_password = get_password_hash(user_in.password)
        # 存入數據庫
        db_user = UserDB(
            username=user_in.username,
            email=user_in.email,
            hashed_password=hashed_password
        )
        db.add(db_user)
        db.commit()
        db.refresh(db_user)
        # 轉換為響應模型
        return UserOut(
            id=db_user.id,
            username=db_user.username,
            email=db_user.email
        )
  3. 修改路由:注入數據庫會話:

    # main.py(修改後)
    from fastapi import FastAPI, Depends
    from sqlalchemy.orm import Session
    from models import UserCreate, UserOut, Base, UserDB
    from crud import create_user
    from database import get_db  # 數據庫依賴(比如獲取Session)
    
    app = FastAPI()
    
    @app.post("users", response_model=UserOut, status_code=201)
    def create_user_route(user_data: UserCreate, db: Session = Depends(get_db)):
        return create_user(db, user_data)

此時運行測試,test_password_hashed會通過——這就是迭代擴展:每加一個功能,都用測試覆蓋,確保不破壞原有功能。

5. 迭代循環:從單接口到複雜場景

5.1 示例:用户認證接口的迭代

假設我們要做“用户登錄接口”,迭代流程如下:

  1. 契約定義:請求模型UserLoginemail+password),響應模型Tokenaccess_token+token_type);
  2. :寫測試test_login_success(期望返回200和Token,但接口未實現,測試失敗);
  3. :寫路由/login/,驗證密碼是否正確,返回Token;
  4. 重構:把認證邏輯抽到auth.py中;
  5. 擴展:加“Token過期時間”需求,新增測試test_token_expired,修改代碼。

5.2 流程圖:迭代式流程的閉環

graph TD
    A[需求分析] --> B[定義接口契約(Pydantic)]
    B --> C[寫失敗的測試]
    C --> D[實現最小可用接口]
    D --> E[運行測試→通過]
    E --> F[重構代碼(優化結構)]
    F --> G[擴展需求(如加密碼哈希)]
    G --> A[需求分析]

6. 課後Quiz

問題1:為什麼在迭代式TDD中,要先定義Pydantic模型而不是直接寫路由?

答案
:Pydantic模型是接口的“契約”——它明確了“客户端要傳什麼”“服務器要返回什麼”。如果先寫路由再補模型,容易出現“接口返回的字段和客户端預期不一致”的問題(比如客户端期待user_id
,但路由返回id),後期修改成本很高。先定義模型,相當於“先和客户端籤合同”,再按合同幹活。

問題2:測試時返回422錯誤,可能的原因是什麼?如何排查?

答案:422錯誤是“請求數據不符合Pydantic模型約束”,常見原因:

  • 缺少必填字段(比如UserCreatepassword沒傳);
  • 字段類型錯誤(比如給age字段傳字符串);
  • 格式不符合要求(比如email字段傳了無效郵箱)。

排查步驟

  1. 看測試中的請求數據,對比Pydantic模型的字段;
  2. 用FastAPI的/docs接口測試,看返回的具體錯誤信息(比如“field required”或“value is not a valid email”);
  3. 檢查模型的驗證規則(比如Field(min_length=8)是否被遵守)。

7. 常見報錯解決方案

報錯1:422 Unprocessable Entity(Validation Error)

原因:請求數據不符合Pydantic模型的約束(比如UserCreatepassword長度不足8位)。
解決

  1. 檢查請求數據的字段名、類型、格式是否和模型一致;
  2. print(response.json())看具體錯誤信息(比如“password must be at least 8 characters”)。
    預防:在測試中覆蓋所有驗證場景(比如測試“密碼長度不足8位”時返回422)。

報錯2:500 Internal Server Error

原因:接口實現中有未捕獲的異常(比如數據庫查詢時user = db.query(UserDB).first()返回None,後續調用user.id
會報錯)。
解決

  1. 看FastAPI的日誌(運行時加--reload參數),定位異常位置;
  2. 在代碼中加try-except
    塊捕獲異常,返回有意義的狀態碼(比如raise HTTPException(status_code=404, detail="User not found"))。
    預防:編寫測試覆蓋異常場景(比如測試“查詢不存在的用户”時返回404)。

報錯3:404 Not Found

原因:測試中的URL和路由定義不一致(比如路由是/users/,但測試用了/user/)。
解決:複製粘貼路由的路徑到測試中,避免手敲錯誤。
預防:用app.url_path_for("create_user_route")
獲取路由路徑(比如client.post(app.url_path_for("create_user_route"), json=user_data))。

第三方庫版本説明

  • fastapi==0.109.0(FastAPI最新穩定版)
  • pydantic==2.5.3(Pydantic v2,支持更嚴格的驗證)
  • pytest==7.4.4(Python測試框架)
  • httpx==0.26.0(TestClient依賴)
  • sqlalchemy==2.0.25(ORM框架,用於數據庫操作)
  • passlib==1.7.4(密碼哈希庫)
  • python-multipart==0.0.6(處理表單數據,可選)

安裝命令

pip install fastapi pydantic pytest httpx sqlalchemy passlib python-multipart

餘下文章內容請點擊跳轉至 個人博客頁面 或者 掃碼關注或者微信搜一搜:編程智域 前端至全棧交流與成長
,閲讀完整的文章:為什麼TDD能讓你的FastAPI開發飛起來?

<details>
<summary>往期文章歸檔</summary>

  • 如何用FastAPI玩轉多模塊測試與異步任務,讓代碼不再“鬧脾氣”? - cmdragon's Blog
  • 如何在FastAPI中玩轉“時光倒流”的數據庫事務回滾測試?
  • 如何在FastAPI中優雅地模擬多模塊集成測試? - cmdragon's Blog
  • 多環境配置切換機制能否讓開發與生產無縫銜接? - cmdragon's Blog
  • 如何在 FastAPI 中巧妙覆蓋依賴注入並攔截第三方服務調用? - cmdragon's Blog
  • 為什麼你的單元測試需要Mock數據庫才能飛起來? - cmdragon's Blog
  • 如何在FastAPI中巧妙隔離依賴項,讓單元測試不再頭疼? - cmdragon's Blog
  • 如何在FastAPI中巧妙隔離依賴項,讓單元測試不再頭疼? - cmdragon's Blog
  • 測試覆蓋率不夠高?這些技巧讓你的FastAPI測試無懈可擊! - cmdragon's Blog
  • 為什麼你的FastAPI測試覆蓋率總是低得讓人想哭? - cmdragon's Blog
  • 如何讓FastAPI測試不再成為你的噩夢? - cmdragon's Blog
  • FastAPI測試環境配置的秘訣,你真的掌握了嗎? - cmdragon's Blog
  • 全鏈路追蹤如何讓FastAPI微服務架構的每個請求都無所遁形? - cmdragon's Blog
  • 如何在API高併發中玩轉資源隔離與限流策略? - cmdragon's Blog
  • 任務分片執行模式如何讓你的FastAPI性能飆升? - cmdragon's Blog
  • 冷熱任務分離:是提升Web性能的終極秘籍還是技術噱頭? - cmdragon's Blog
  • 如何讓FastAPI在百萬級任務處理中依然遊刃有餘? - cmdragon's Blog
  • 如何讓FastAPI與消息隊列的聯姻既甜蜜又可靠? - cmdragon's Blog
  • 如何在FastAPI中巧妙實現延遲隊列,讓任務乖乖等待? - cmdragon's Blog
  • FastAPI的死信隊列處理機制:為何你的消息系統需要它? - cmdragon's Blog
  • 如何讓FastAPI任務系統在失敗時自動告警並自我修復? - cmdragon's Blog
  • 如何用Prometheus和FastAPI打造任務監控的“火眼金睛”? - cmdragon's Blog
  • 如何用APScheduler和FastAPI打造永不宕機的分佈式定時任務系統? - cmdragon's Blog
  • 如何在 FastAPI 中玩轉 APScheduler,讓任務定時自動執行? - cmdragon's Blog
  • 定時任務系統如何讓你的Web應用自動完成那些煩人的重複工作? - cmdragon's Blog
  • Celery任務監控的魔法背後藏着什麼秘密? - cmdragon's Blog
  • 如何讓Celery任務像VIP客户一樣享受優先待遇? - cmdragon's Blog
  • 如何讓你的FastAPI Celery Worker在壓力下優雅起舞? - cmdragon's Blog
  • FastAPI與Celery的完美邂逅,如何讓異步任務飛起來? - cmdragon's Blog
  • FastAPI消息持久化與ACK機制:如何確保你的任務永不迷路? - cmdragon's Blog
  • FastAPI的BackgroundTasks如何玩轉生產者-消費者模式? - cmdragon's Blog
  • BackgroundTasks 還是 RabbitMQ?你的異步任務到底該選誰? - cmdragon's Blog
  • BackgroundTasks與Celery:誰才是異步任務的終極贏家? - cmdragon's Blog
  • 如何在 FastAPI 中優雅處理後台任務異常並實現智能重試? - cmdragon's Blog
  • BackgroundTasks 如何巧妙駕馭多任務併發? - cmdragon's Blog
  • 如何讓FastAPI後台任務像多米諾骨牌一樣井然有序地執行? - cmdragon's Blog
  • FastAPI後台任務:是時候讓你的代碼飛起來了嗎? - cmdragon's Blog

</details>

<details>
<summary>免費好用的熱門在線工具</summary>

  • 歌詞生成工具 - 應用商店 | By cmdragon
  • 網盤資源聚合搜索 - 應用商店 | By cmdragon
  • ASCII字符畫生成器 - 應用商店 | By cmdragon
  • JSON Web Tokens 工具 - 應用商店 | By cmdragon
  • Bcrypt 密碼工具 - 應用商店 | By cmdragon
  • GIF 合成器 - 應用商店 | By cmdragon
  • GIF 分解器 - 應用商店 | By cmdragon
  • 文本隱寫術 - 應用商店 | By cmdragon
  • CMDragon 在線工具 - 高級AI工具箱與開發者套件 | 免費好用的在線工具
  • 應用商店 - 發現1000+提升效率與開發的AI工具和實用程序 | 免費好用的在線工具
  • CMDragon 更新日誌 - 最新更新、功能與改進 | 免費好用的在線工具
  • 支持我們 - 成為贊助者 | 免費好用的在線工具
  • AI文本生成圖像 - 應用商店 | 免費好用的在線工具
  • 臨時郵箱 - 應用商店 | 免費好用的在線工具
  • 二維碼解析器 - 應用商店 | 免費好用的在線工具
  • 文本轉思維導圖 - 應用商店 | 免費好用的在線工具
  • 正則表達式可視化工具 - 應用商店 | 免費好用的在線工具
  • 文件隱寫工具 - 應用商店 | 免費好用的在線工具
  • IPTV 頻道探索器 - 應用商店 | 免費好用的在線工具
  • 快傳 - 應用商店 | 免費好用的在線工具
  • 隨機抽獎工具 - 應用商店 | 免費好用的在線工具
  • 動漫場景查找器 - 應用商店 | 免費好用的在線工具
  • 時間工具箱 - 應用商店 | 免費好用的在線工具
  • 網速測試 - 應用商店 | 免費好用的在線工具
  • AI 智能摳圖工具 - 應用商店 | 免費好用的在線工具
  • 背景替換工具 - 應用商店 | 免費好用的在線工具
  • 藝術二維碼生成器 - 應用商店 | 免費好用的在線工具
  • Open Graph 元標籤生成器 - 應用商店 | 免費好用的在線工具
  • 圖像對比工具 - 應用商店 | 免費好用的在線工具
  • 圖片壓縮專業版 - 應用商店 | 免費好用的在線工具
  • 密碼生成器 - 應用商店 | 免費好用的在線工具
  • SVG優化器 - 應用商店 | 免費好用的在線工具
  • 調色板生成器 - 應用商店 | 免費好用的在線工具
  • 在線節拍器 - 應用商店 | 免費好用的在線工具
  • IP歸屬地查詢 - 應用商店 | 免費好用的在線工具
  • CSS網格佈局生成器 - 應用商店 | 免費好用的在線工具
  • 郵箱驗證工具 - 應用商店 | 免費好用的在線工具
  • 書法練習字帖 - 應用商店 | 免費好用的在線工具
  • 金融計算器套件 - 應用商店 | 免費好用的在線工具
  • 中國親戚關係計算器 - 應用商店 | 免費好用的在線工具
  • Protocol Buffer 工具箱 - 應用商店 | 免費好用的在線工具
  • IP歸屬地查詢 - 應用商店 | 免費好用的在線工具
  • 圖片無損放大 - 應用商店 | 免費好用的在線工具
  • 文本比較工具 - 應用商店 | 免費好用的在線工具
  • IP批量查詢工具 - 應用商店 | 免費好用的在線工具
  • 域名查詢工具 - 應用商店 | 免費好用的在線工具
  • DNS工具箱 - 應用商店 | 免費好用的在線工具
  • 網站圖標生成器 - 應用商店 | 免費好用的在線工具
  • XML Sitemap

</details>

user avatar youyudeshangpu_cny857 Avatar tdengine Avatar tully_l Avatar winfacter Avatar renxingdebenma Avatar
Favorites 5 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.