問題
各框架的「官方測試路徑」(make test 之類)常常是壞的,或者跟 CI 不一致。結果一堆環境性失敗被誤判成程式碼問題,時間都花在跟環境纏鬥。
下面按語言記下我實測踩到的坑。每條我會標來源:有官方文件佐證的附連結,只有實測的標「實測」,兩者都沒有的標「未查證」。
Django(Python/pytest/Docker)
-
host port 被別的行程佔用,測試容器起不來。單元測試把 cache mock 掉、根本不需要真的 redis,所以改用:
docker compose run --rm --no-deps web pytestdocker compose run預設不建立 service 設定裡的 ports,--no-deps不會順帶啟動相依的容器(docker compose run 文件)。實測整套測試在這個指令下全綠。 -
需要 redis 的 integration test:
REDIS_URL=redis://redis:6379走 compose 的服務名,host port 被誰佔都不影響。如果是 host port 衝突讓 redis 起不來,在 override 檔用ports: !reset []讓 redis 只在內部網路可見。注意 compose 對 list 欄位是附加合併而不是取代,所以一定要用!reset/!override(Compose 合併規則)。實測可行。 -
CACHE_BACKEND設成 locmem 時的陷阱:.env裡CACHE_BACKEND=locmem時,Redis client 的測試會拿到 locmem 的 LOCATION('unique-snowflake')當成 redis URL,直接ValueError。跑 redis integration test 要加-e CACHE_BACKEND=redis。(從設定檔讀出來的結論,未另外查證) -
MySQL 測試庫要授權:Django 建測試庫時,用的就是
DATABASES裡設定的那個帳號,所以帳號要有建庫的權限;測試庫名預設是在NAME前面加test_(Django 測試文件)。沒授權的話@django_db一建庫就 Access denied。我原本的筆記寫的是
GRANT ALL ON test_*.*,回頭查 MySQL 文件發現這個寫法不對:資料庫層級的 GRANT 認的萬用字元是%和_,不是*;而且_本身就是萬用字元,要當字面值得寫成\_。另外,這種萬用字元用法從 MySQL 8.0.35 起已標為 deprecated(MySQL GRANT 文件)。所以比較穩的寫法是:-- 只授權給測試庫;'app' 換成你的應用帳號 GRANT ALL ON `test\_%`.* TO 'app'@'%'; -- 或者不用萬用字元,直接點名 Django 會建的那一個測試庫 GRANT ALL ON `test_myapp`.* TO 'app'@'%';(未查證:修正後的語法我沒有在原環境重跑過)
.NET(.NET 8/xUnit/Docker)
-
web 容器是
target: production(沒有 SDK),所以走 web 容器的make test/make lint(dotnet test、dotnet format)全部跑不動——明明 Dockerfile 裡就有developmentstage。修法是 compose 的 web 改成target: development。(實測) -
development target 沒有掛 source volume:程式碼是 build 時 COPY 進 image 的,改了 code 根本進不了容器。症狀超騙人:改了測試,跑出來還是舊結果,stack trace 指向舊的行號。加上
volumes: - .:/app - /app/bin - /app/obj - /app/tests/bin - /app/tests/obj用匿名 volume 蓋掉 bin/obj,避免容器裡產生的檔案(root 擁有)污染 host。加完之後整套測試全綠。(實測)
-
pre-commit 會先把未 stage 的改動 stash 起來再跑 hook。官方原話是 pre-commit「只對 staged 內容執行,執行 hook 期間會暫時 stash 未 staged 的改動」(pre-commit.com)。所以只 commit 一個檔時,其他還沒 stage 的修正會被暫時收走,hook 檢查的樹又變回舊狀態。多檔修正要一起 stage 進同一個 commit,讓 commit 出去的樹一次就是綠的(例如測試修正和 lint 修正放同一個 commit)。
-
dotnet format --verify-no-changes被命名規則擋下:private static readonly 欄位用 PascalCase、缺_前綴,觸發 IDE1006(Naming rule violation),format 也不會自動幫你改名,只能手動 rename 成_camelCase。文件能佐證的部分:
--verify-no-changes在有檔案會被改動時以非零 exit code 結束(dotnet format 文件);IDE1006 是所有命名規則共用的 rule ID,要在 build 時強制需要把嚴重度設成 warning 或 error(.NET 命名規則文件)。「_前綴」是該 repo.editorconfig的慣例,不是 .NET 預設;exit code 剛好是 2 則是實測觀察,文件只寫「非零」。
通用:SSE 要排除在壓縮和 request timeout 之外
我在三個語言的服務上都遇到同一件事(實測):壓縮會把 text/event-stream 緩衝住、事件不即時送到前端;request timeout 會把 SSE 長連線砍斷。
- Django:GZip middleware 寫個子類,
response.streaming為真就跳過。(實測;Django 文件沒有寫到串流回應的壓縮行為,未查證) - NestJS:compression 的 filter 濾掉
text/event-stream,timeout interceptor 排除/sse路徑。Node 的compression套件 README 自己就寫了它「無法直接搭配 server-sent events」,因為壓縮需要先緩衝一段輸出(expressjs/compression)。 - .NET:內建 ResponseCompression 的預設 MIME 清單(
application/javascript、application/json、application/xml、text/css、text/html、text/json、text/plain、text/xml)本來就不含text/event-stream(ASP.NET Core Response compression);自製的 timeout middleware 則用ExcludedPaths排除 SSE 路徑。
為什麼這樣解
這些坑的共同根源是:dev 環境的 compose/設定本身有 regression,或者跟 CI 不一致——跟「文件沒跟上程式碼」是同一種病。先花時間把環境弄對,比在錯的環境上瞎猜快得多。
怎麼預防再次發生
- 接手一個框架的第一件事:確認官方的
make test真的能跑。不能跑就先修環境,多半是 compose 的 target、volume 或.env。 - 同一個環境問題試了兩種方法都沒進展,就停下來換個角度(查文件、縮小重現範圍、找人問),不要硬鑽。我在 .NET 環境上就是纏鬥太久才回頭,那段時間幾乎都是白費的。
- 修好的環境 regression(例如 compose target)值得 commit 回 repo,但要在 commit 訊息講清楚「順手改了共用設定」。