【GitHub Actions CI 實戰】#03 後端自動化:FastAPI + Ruff + Pytest 完整檢查

測驗:GitHub Actions CI 實戰 – 後端自動化

共 5 題,點選答案後會立即顯示結果

1. Ruff 相較於傳統的 flake8 + black 組合,主要優勢是什麼?

  • A. 支援更多程式語言
  • B. 執行速度快 10-100 倍
  • C. 可以自動產生測試程式碼
  • D. 內建 pytest 整合

2. 在 pyproject.toml 的 Ruff 設定中,select = ["E", "F", "I"] 代表什麼意思?

  • A. 選擇錯誤等級為 E、F、I 的警告
  • B. 啟用 extra、fast、import 三種模式
  • C. 啟用 pycodestyle (E)、pyflakes (F)、isort (I) 規則
  • D. 設定三個不同的檢查階段

3. FastAPI 的 TestClient 主要用途是什麼?

  • A. 在瀏覽器中自動測試 API
  • B. 在不啟動伺服器的情況下測試 API 端點
  • C. 產生 API 文件
  • D. 監控 API 效能

4. 在 GitHub Actions 的 Matrix Strategy 中設定 fail-fast: false 的作用是什麼?

  • A. 讓所有測試版本同時開始執行
  • B. 加速測試執行速度
  • C. 只執行最新的 Python 版本
  • D. 某個版本失敗時,其他版本繼續執行完畢

5. 在 CI 環境中執行 Ruff 時,為什麼要使用 ruff format --check . 而不是 ruff format .

  • A. --check 只檢查不修改檔案,有問題會讓 CI 失敗
  • B. --check 會產生詳細的檢查報告
  • C. --check 執行速度比較快
  • D. --check 只檢查語法錯誤

一句話說明

用 GitHub Actions 自動執行 Python 程式碼檢查(Ruff)和測試(Pytest),確保每次 push 都不會壞掉。


這篇會用到什麼

工具 用途
Ruff 檢查程式碼格式和潛在問題(取代 flake8 + black)
Pytest 執行測試
TestClient 測試 FastAPI 端點
Matrix Strategy 同時測試多個 Python 版本

Ruff:為什麼比 flake8 + black 更快

一句話說明

Ruff 是用 Rust 寫的 Python linter,做的事跟 flake8 + black + isort 一樣,但快 10-100 倍。

最小範例

# 安裝
pip install ruff

# 檢查程式碼
ruff check .

# 自動修復
ruff check --fix .

# 格式化(取代 black)
ruff format .
Code language: PHP (php)

pyproject.toml 設定

[tool.ruff]
line-length = 88           # 每行最多 88 字元(跟 black 一樣)
target-version = "py311"   # 目標 Python 版本

[tool.ruff.lint]
select = ["E", "F", "I"]   # 啟用的規則:E=pycodestyle, F=pyflakes, I=isort
ignore = ["E501"]          # 忽略的規則:E501 是行太長

[tool.ruff.format]
quote-style = "double"     # 用雙引號
Code language: PHP (php)

翻譯

  • select = ["E", "F", "I"]:檢查基本錯誤(E)、未使用變數(F)、import 順序(I)
  • ignore = ["E501"]:不管「行太長」這個警告

常見規則代碼

代碼 意思
E4xx 縮排問題
E7xx 語法錯誤
F401 import 了但沒用
F841 變數定義了但沒用
I001 import 順序不對

FastAPI 測試:TestClient 基本用法

一句話說明

TestClient 讓你在不啟動伺服器的情況下測試 API 端點。

最小範例

# test_main.py
from fastapi.testclient import TestClient
from main import app  # 你的 FastAPI app

client = TestClient(app)

def test_read_root():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"message": "Hello"}
Code language: PHP (php)

這段代碼做了什麼

  1. 建立一個假的 HTTP client
  2. / 發送 GET 請求
  3. 確認回傳 200 和正確的 JSON

逐行翻譯

from fastapi.testclient import TestClient   # 引入測試工具
from main import app                         # 引入你的 app

client = TestClient(app)                     # 建立測試用的 client

def test_read_root():                        # 測試函式名稱要以 test_ 開頭
    response = client.get("/")               # 發送 GET 請求到 /
    assert response.status_code == 200       # 確認狀態碼是 200
    assert response.json() == {"message": "Hello"}  # 確認回傳內容
Code language: PHP (php)

常見變化

變化 1:測試 POST 請求

def test_create_item():
    response = client.post(
        "/items/",
        json={"name": "Test", "price": 100}  # 送 JSON 資料
    )
    assert response.status_code == 201
    assert response.json()["name"] == "Test"
Code language: PHP (php)

翻譯:發送 POST 請求,帶著 JSON 資料

變化 2:用 fixture 共用 client

import pytest

@pytest.fixture
def client():
    return TestClient(app)

def test_example(client):  # client 會自動注入
    response = client.get("/")
    assert response.status_code == 200
Code language: PHP (php)

翻譯:用 @pytest.fixture 讓多個測試共用同一個 client


Pytest 設定與常用參數

一句話說明

Pytest 是 Python 測試框架,自動找出所有 test_*.py 檔案並執行。

常用執行參數

# 執行所有測試
pytest

# 顯示詳細輸出
pytest -v

# 顯示 print 輸出
pytest -s

# 只執行特定檔案
pytest tests/test_api.py

# 只執行特定測試
pytest tests/test_api.py::test_read_root

# 失敗就停止
pytest -x

# 平行執行(需安裝 pytest-xdist)
pytest -n auto
Code language: PHP (php)

pyproject.toml 設定

[tool.pytest.ini_options]
testpaths = ["tests"]        # 測試檔案放在 tests/ 目錄
python_files = "test_*.py"   # 測試檔案命名規則
python_functions = "test_*"  # 測試函式命名規則
addopts = "-v --tb=short"    # 預設參數:詳細輸出、簡短錯誤訊息
Code language: PHP (php)

專案結構

my_project/
├── main.py              # FastAPI app
├── pyproject.toml       # 設定檔
├── requirements.txt     # 依賴
└── tests/
    ├── __init__.py      # 空檔案,讓 tests 成為 package
    ├── test_main.py     # 測試主要功能
    └── conftest.py      # 共用的 fixture
Code language: PHP (php)

撰寫後端 CI Workflow

完整範例

# .github/workflows/backend-ci.yml
name: Backend CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint-and-test:
    runs-on: ubuntu-latest

    steps:
      # 1. 取得程式碼
      - uses: actions/checkout@v4

      # 2. 設定 Python
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"
          cache: "pip"  # 快取 pip 依賴

      # 3. 安裝依賴
      - name: Install dependencies
        run: |
          pip install -r requirements.txt
          pip install ruff pytest httpx

      # 4. 執行 Ruff 檢查
      - name: Lint with Ruff
        run: |
          ruff check .
          ruff format --check .

      # 5. 執行測試
      - name: Test with Pytest
        run: pytest -v
Code language: PHP (php)

逐段翻譯

on:
  push:
    branches: [main]      # main 分支有 push 時觸發
  pull_request:
    branches: [main]      # 有 PRmain 時觸發
Code language: CSS (css)
- uses: actions/setup-python@v5
  with:
    python-version: "3.11"  # 使用 Python 3.11
    cache: "pip"            # 自動快取 pip 依賴
Code language: PHP (php)

翻譯:設定 Python 環境並啟用快取,下次執行會更快

- name: Lint with Ruff
  run: |
    ruff check .           # 檢查程式碼問題
    ruff format --check .  # 檢查格式(不修改,只報錯)
Code language: PHP (php)

翻譯--check 表示只檢查不修改,有問題會讓 CI 失敗


Matrix Strategy:多 Python 版本測試

一句話說明

Matrix 讓你同時測試多個 Python 版本,確保程式碼相容性。

完整範例

name: Backend CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.10", "3.11", "3.12"]  # 測試這三個版本
      fail-fast: false  # 一個版本失敗不影響其他版本

    steps:
      - uses: actions/checkout@v4

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: "pip"

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Run tests
        run: pytest -v
Code language: PHP (php)

逐段翻譯

strategy:
  matrix:
    python-version: ["3.10", "3.11", "3.12"]
  fail-fast: false
Code language: JavaScript (javascript)

翻譯

  • 會產生 3 個平行執行的 job
  • fail-fast: false 表示某個版本失敗時,其他版本繼續跑完
python-version: ${{ matrix.python-version }}
Code language: HTTP (http)

翻譯${{ matrix.python-version }} 會被替換成 “3.10”、”3.11″、”3.12″

GitHub Actions 介面呈現

執行時會看到:

test (3.10) ✓
test (3.11) ✓
test (3.12) ✗  <- 這個失敗了
Code language: CSS (css)

快取 pip 依賴加速 CI

方法 1:使用 setup-python 內建快取(推薦)

- uses: actions/setup-python@v5
  with:
    python-version: "3.11"
    cache: "pip"  # 就這一行
Code language: PHP (php)

這會自動:

  1. requirements.txt 的 hash 當快取 key
  2. 下次執行時如果 requirements.txt 沒變,直接用快取

方法 2:指定快取依據檔案

- uses: actions/setup-python@v5
  with:
    python-version: "3.11"
    cache: "pip"
    cache-dependency-path: |
      requirements.txt
      requirements-dev.txt
Code language: JavaScript (javascript)

翻譯:如果有多個 requirements 檔案,用 cache-dependency-path 指定

效果

情況 安裝時間
無快取 30-60 秒
有快取 5-10 秒

完整專案範例

檔案結構

my-fastapi-project/
├── .github/
│   └── workflows/
│       └── ci.yml
├── main.py
├── tests/
│   ├── __init__.py
│   └── test_main.py
├── pyproject.toml
└── requirements.txt

main.py

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "Hello"}

@app.get("/items/{item_id}")
def read_item(item_id: int):
    return {"item_id": item_id}
Code language: JavaScript (javascript)

tests/test_main.py

from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_read_root():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"message": "Hello"}

def test_read_item():
    response = client.get("/items/42")
    assert response.status_code == 200
    assert response.json() == {"item_id": 42}
Code language: JavaScript (javascript)

pyproject.toml

[project]
name = "my-fastapi-project"
version = "0.1.0"
requires-python = ">=3.10"

[tool.ruff]
line-length = 88
target-version = "py311"

[tool.ruff.lint]
select = ["E", "F", "I"]
ignore = []

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v"
Code language: JavaScript (javascript)

requirements.txt

fastapi>=0.100.0
uvicorn>=0.23.0
httpx>=0.24.0  # TestClient 需要
pytest>=7.4.0
ruff>=0.1.0
Code language: PHP (php)

.github/workflows/ci.yml

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install Ruff
        run: pip install ruff

      - name: Run Ruff
        run: |
          ruff check .
          ruff format --check .

  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.10", "3.11", "3.12"]

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: "pip"

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Run tests
        run: pytest -v
Code language: JavaScript (javascript)

Vibe Coder 檢查點

看到後端 CI workflow 時確認:

  • [ ] Ruff 有設定 --check 嗎?(CI 只檢查不修改)
  • [ ] TestClient 有安裝 httpx 嗎?(FastAPI 的 TestClient 依賴它)
  • [ ] 測試函式名稱有以 test_ 開頭嗎?
  • [ ] Matrix 有設定 fail-fast: false 嗎?(避免一個版本失敗就全停)
  • [ ] 有啟用 cache: "pip" 加速 CI 嗎?

延伸:知道就好

這些進階功能遇到再查:

  • pytest-cov:產生測試覆蓋率報告
  • pytest-asyncio:測試 async 函式
  • Ruff 的 --fix:自動修復問題(本地開發用,CI 不用)
  • pre-commit:在 commit 前自動執行 Ruff
  • uv:比 pip 更快的套件管理器,可用 uv pip install 取代 pip install

總結

工具 用途 CI 指令
Ruff 檢查格式和問題 ruff check . && ruff format --check .
Pytest 執行測試 pytest -v
TestClient 測試 API 端點 在測試檔案中使用
Matrix 多版本測試 strategy.matrix.python-version
cache: pip 加速安裝 actions/setup-python 參數

下一篇我們會介紹如何將前後端 CI 整合,並處理更複雜的 monorepo 情境。

進階測驗:GitHub Actions CI 實戰 – 後端自動化

測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。

1. 你正在為 FastAPI 專案設定 CI,希望同時測試 Python 3.10、3.11、3.12 三個版本。應該如何設定 workflow? 情境題

  • A. 建立三個獨立的 workflow 檔案,各自指定不同的 Python 版本
  • B. 使用 strategy.matrix.python-version 設定陣列,搭配 ${{ matrix.python-version }} 引用
  • C. 在同一個 job 中執行三次 actions/setup-python
  • D. 使用 needs 讓三個版本依序執行

2. 團隊成員回報 CI 執行時間過長,安裝 pip 依賴就花了 50 秒。你應該如何優化? 情境題

  • A. 減少 requirements.txt 中的依賴數量
  • B. 將依賴安裝步驟移到測試之後
  • C. 在 actions/setup-python 中加入 cache: "pip" 參數
  • D. 改用 pip install --fast 參數

3. 執行測試時出現以下錯誤,最可能的原因是什麼? 錯誤診斷

$ pytest -v ModuleNotFoundError: No module named ‘httpx’
  • A. pytest 版本太舊,不支援 FastAPI
  • B. FastAPI 的 TestClient 依賴 httpx,但尚未安裝
  • C. 測試檔案放錯目錄
  • D. Python 版本不相容

4. CI 執行 Ruff 檢查時出現以下錯誤,應該如何修正? 錯誤診斷

main.py:3:1: F401 [*] `os` imported but unused Found 1 error. [*] 1 fixable with the `–fix` option.
  • A. 在 CI 中執行 ruff check --fix . 自動修復
  • B. 在 pyproject.toml 中加入 ignore = ["F401"]
  • C. 更新 Ruff 到最新版本
  • D. 在本地移除未使用的 import,然後 commit 並 push

5. 你想讓 lint 檢查和測試分開執行,即使 lint 失敗也要執行測試。以下 workflow 結構正確嗎? 情境題

jobs: lint: runs-on: ubuntu-latest steps: – uses: actions/checkout@v4 – run: ruff check . test: runs-on: ubuntu-latest steps: – uses: actions/checkout@v4 – run: pytest -v
  • A. 正確,兩個 job 會平行執行,互不影響
  • B. 錯誤,需要加入 needs: lint 才能執行
  • C. 錯誤,兩個 job 會因為共用 checkout 而衝突
  • D. 錯誤,應該把 lint 和 test 放在同一個 job 的不同 step

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *