測驗:GitHub Actions CI 實戰 – 後端自動化
共 5 題,點選答案後會立即顯示結果
1. Ruff 相較於傳統的 flake8 + black 組合,主要優勢是什麼?
2. 在 pyproject.toml 的 Ruff 設定中,select = ["E", "F", "I"] 代表什麼意思?
3. FastAPI 的 TestClient 主要用途是什麼?
4. 在 GitHub Actions 的 Matrix Strategy 中設定 fail-fast: false 的作用是什麼?
5. 在 CI 環境中執行 Ruff 時,為什麼要使用 ruff format --check . 而不是 ruff format .?
一句話說明
用 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)這段代碼做了什麼:
- 建立一個假的 HTTP client
- 對
/發送 GET 請求 - 確認回傳 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] # 有 PR 到 main 時觸發
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)這會自動:
- 用
requirements.txt的 hash 當快取 key - 下次執行時如果 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 題,包含情境題與錯誤診斷題。
共 5 題,包含情境題與錯誤診斷題。
1. 你正在為 FastAPI 專案設定 CI,希望同時測試 Python 3.10、3.11、3.12 三個版本。應該如何設定 workflow? 情境題
2. 團隊成員回報 CI 執行時間過長,安裝 pip 依賴就花了 50 秒。你應該如何優化? 情境題
3. 執行測試時出現以下錯誤,最可能的原因是什麼? 錯誤診斷
$ pytest -v
ModuleNotFoundError: No module named ‘httpx’
4. CI 執行 Ruff 檢查時出現以下錯誤,應該如何修正? 錯誤診斷
main.py:3:1: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 fixable with the `–fix` option.
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