本章目標
學會為一個改動選擇最小且足夠的測試,並把「程式碼/本機聚焦/整合/E2E/Apifox/部署現場」證據分開報告。命令依目前 Makefile、Go tests 與前端 package scripts 核實;本輪沒有執行業務測試或部署驗收。
前置條件
先讀後端開發規範及前端頁面規範。確認受影響的契約、租戶、資料庫 migration、環境和既有工作區差異;不使用真實密碼、token、內部帳號或 production data 當 fixture。
證據分層
| 層級 | 主要問題 | 目前入口 | 不能證明 |
|---|---|---|---|
| static/type | 形狀、生成物、命名與編譯 | Go make ci-core、前端 pnpm typecheck、pnpm lint | runtime 資料與登入 |
| unit | 純函式、service 分支、元件狀態 | make unit、另跑 go test ./tests/unit/...、Vitest | 真實 PostgreSQL/外部服務 |
| contract | OpenAPI、SQL、migration、路由註冊 | make contract、前端 contract tests | 部署版本行為 |
| integration | 真 DB、RLS、transaction、worker | make integration(testcontainers) | 對外環境與 Apifox |
| E2E | compose stack 上的 HTTP 黑箱流程 | make ci-local | 測試站/production、前端頁面 |
| Apifox | 已授權環境的 happy path 與關鍵錯誤 | make apifox-isolated | 未跑的斷言或舊 schema |
| browser/deploy | 真實版本、租戶、登入與畫面 | playwright-lan MCP 四寬驗收與部署記錄 | 其他版本或租戶 |
綠色 unit test 只表示該測試執行的路徑通過。未執行、被 skip、斷言數為零或環境不同,都必須在報告中保留。後端 make integration、make e2e 與 contract 前置的 e2e-static 都經 cmd/testgate -forbid-skip 判定,任何 skip 都算失敗;前端 Vitest 沒有這層保護,要自己看 skipped 數。
後端沒有平台 CI:tests/contract/ci_gate_test.go 禁止加入 .gitlab-ci.yml、.github/workflows 等設定,所有 gate 都在本機以 make 執行。前端 GitLab CI 的 static-check 只跑 pnpm lint 與 pnpm typecheck,pnpm test:run 因既有失敗刻意未納入,所以前端 pipeline 綠不等於測試綠。
後端單元與契約
後端 unit 通常不要求 integration/E2E:
# 工作目錄:nexus-pro-be-plus
go test ./internal/service/... ./internal/api/... -count=1
go test ./tests/unit/... -count=1 # make unit 不包含這個目錄
make contract
make ci-coreMakefile 的 ci-core 依序執行 fmt vet build lint unit contract drift mod-drift migration-structural file-size。其中 unit 只跑 ./cmd/...、./internal/...、./db/...,再用 python3 執行 approved-config-test;tests/unit/...(例如 tests/unit/forminstance)不在 ci-core/ci-local 內,改到相關程式時要另跑 go test ./tests/unit/... 或 make test。drift 比對 sqlc、mapper、config、errcodes 生成物;contract 會先跑 e2e-static,驗證 E2E 素材的封閉契約,不代表真的啟動 compose。第一次執行時,golangci-lint 與 sqlc 會以 go run 下載 Makefile 指定的版本,需要可用的 Go module 來源。
契約測試要鎖:HTTP method/path、request body closed object、permission policy、error code、migration RLS、sqlc 生成狀態和 event handler 1:1 registry(後者由 internal/outbox 的 handler registry 測試負責)。closed object 目前由各域的 OpenAPI helper 檢查;snake_case 命名沒有機械檢查,只能靠 review。不要把實作細節重複到每一個測試;找既有 canonical helper。
新增路由或 migration 時的封閉清單
- 路由:權限點由 handler 註冊時的
route.Point(...)宣告,快照在internal/route/testdata/registry.golden(TestRouteDeclarationSnapshotMatchesGolden逐字比對,沒有自動更新旗標,要手動改);api/openapi.yaml沒有結構化的權限欄位。另外tests/contract/openapi_test.go的trackedOpenAPIOperations與tests/contract/operator_surface_test.go的精確路由集合也要加上新的 path/method,任一漏改make contract都會失敗。 - migration:新檔要在
db/migrations/FROZEN.sha256追加 hash(TestReleasedMigrationHashesMatch),並把tests/integration/postgres/migrations_test.go的latestMigrationVersion改成新版本;編號須連續且有 goose Down。改 queries 後以make generate重生 sqlc 與 mapper,make drift會比對結果。
PostgreSQL 整合與 RLS
make integration 以 REQUIRE_INTEGRATION=1 執行 ./cmd/db-provision 與 ./tests/integration/...(postgres、redis);PostgreSQL 由 testcontainers 在測試中啟動,所以需要 Docker,但不經 compose。tests/integration/postgres 涵蓋真實 transaction、round-trip、RLS、outbox、workflow 與 domain migration。每個 tenant negative case 都要證明資料沒有越界,不可只測 WHERE tenant_id 的 happy path。
make integration
# 聚焦單支時要自己帶旗標,否則測試會 skip
REQUIRE_INTEGRATION=1 go test ./tests/integration/postgres -run '^TestName$' -count=1如果缺 Docker、資料庫或認證,命令失敗就代表環境 gate 受阻,不是產品 code pass。migration 測試尤其需要確認實際套用版本、既有資料與 down/還原策略;新增 migration 還要同步 FROZEN.sha256 與 latestMigrationVersion。
E2E 與 compose
make ci-local 的實際順序:
ci-core。make integration(testcontainers,不經 compose)。compose-up:e2e-scope檢查隔離 project/image 名稱,再跑e2e-static,然後以docker-compose.yml疊加tests/e2e/docker-compose.e2e.yml執行up -d --build --wait --wait-timeout 120 api worker。override 讓 PostgreSQL 使用 tmpfs,對外 port 改為隨機本機埠。make e2e:以REQUIRE_E2E=1執行tests/e2e,同樣經testgate -forbid-skip。compose-clean:down --remove-orphans並刪除該次 image;中途失敗也會執行清理。
compose 的 api/worker healthcheck 打的是 /healthz,所以 --wait 只證明程序存活;API 與 worker 的 /readyz 由 E2E 測試斷言。tests/e2e 是對這個 compose stack 的 HTTP 黑箱測試(含 health/readiness 與資料庫故障注入),不是瀏覽器頁面流程。它證明該次隔離 compose 的流程,不代表共享 test site 或 production。
# 由 Makefile 產生隔離專案與 image;不要手動共用別人的 compose project
make ci-local若只跑 go test ./tests/e2e 而沒有 REQUIRE_E2E=1 與對版 compose,不能宣稱 E2E gate 通過。驗證 worker 時要確認跑的是重建後 binary,不是舊 image。
前端測試與 MSW
業務前端使用 Vitest、Testing Library、MSW、jsdom。MSW handler 只應照 api/openapi.yaml 與 route registry 寫已註冊路由;mock auth/MSW 只允許 development,不能證明 Keycloak、後端授權或 RLS。
前端 AGENTS.md 的交付門是 lint、typecheck、test:run、deadcode 四項;改 theme、SSR 或 App Router 邊界時再加 production build 與 hydration 檢查。指令優先用 nub run,pnpm 亦可。
# 工作目錄:nexus-pro-web-plus
pnpm typecheck
pnpm test:run features/forms/workflow-preview.contract.test.tsx
pnpm test:run libs/api/client.contract.test.ts
pnpm lint
pnpm deadcodedeadcode 是 knip --include exports,types,duplicates --max-issues 8;門檻只隨實測下降同次下調,調高要有 MR 理由。
workflow-preview.contract.test.tsx 示範空 body、camelCase、undetermined、未知 warning code、409 invalid state、404 和 revision revalidation。transport contract 則測 envelope、casing、request ID、204、FormData、blob error 和 schema drift;這些測試不能代替真實登入。
test/contract/openapiShape.test.ts 與 test/contract/routeRegistry.test.ts 會讀同層的 ../nexus-pro-be-plus(OpenAPI 與 internal/route);沒有這個 checkout 時,前後端對照的斷言以 describe.skipIf 整組跳過,報告要寫 skipped,不能算通過。test/fixtures/routeRegistry.json 是後端路由宣告的副本,後端改頁樹、群組、跨端入口或權限點後,要執行 node scripts/sync-page-tree.mjs 並把 diff 一起送審。
瀏覽器與可及性
瀏覽器驗證要記錄 URL、build/version、登入租戶、viewport、操作步驟、API status/trace、console error 與畫面結果。前端規範要求 UI 交付必驗 1440、1024、720、360 四個寬度,每個寬度都要有截圖、console 與流程證據,缺任一寬度即是交付缺口;窄寬時表格要自身橫捲而不是 body overflow,並確認 focus 可見、鍵盤可用、錯誤非只靠顏色。
瀏覽器自動化、截圖與 console/network 檢查統一使用已綁定的 playwright-lan MCP,不啟動本機自動化瀏覽器。MCP 的瀏覽器在服務端,localhost 指向服務端而不是你的機器,要改用 MCP 可達的位址;MCP 或網路不可用時標記 BLOCKED,繼續其他可獨立執行的檢查。
# 前端本地啟動形狀;交給 MCP 驗收前先確認它連得到這個位址
pnpm dev未登入被 redirect、後端 401、MSW fixture 或 selector 找不到,應在證據中說明阻塞原因;不能截圖後推測功能已完成。
Apifox 聯調
後端每次新增或修改可呼叫 endpoint,都要在 Apifox 建/改場景並加入套件 29010。交付驗證用 make apifox-isolated:它建置目前工作樹的獨立 compose,以 apifoxfixture build tag 的 TestPrepareApifox 建立管理員、self-only 員工與確定的假勤資料,建立只含本次 loopback origin 的臨時雲端環境後執行 make apifox-test,最後刪除臨時環境並清理 compose。它會 unset 繼承的 NEXUS_DEV_PASSWORD 等變數,身分全由私有 fixture 提供;前提是 Docker、已登入的 Apifox CLI、既有 .env 與專案的雲端寫入授權。
# 工作目錄:nexus-pro-be-plus;本輪未執行
make apifox-isolated
# 手動執行:APIFOX_RUNTIME_VARS 指向私有 fixture JSON,APIFOX_ENV 指向與它匹配的環境
APIFOX_RUNTIME_VARS=/path/to/private/runtime.json make apifox-test APIFOX_ENV=<匹配的環境 id>make apifox-test 由 tools/run_apifox.py 執行:除了登入密碼,還要求 myleave/attendance 等 fixture 變數,缺任何一個就在送出請求前以 exit 2 拒絕,所以只設 NEXUS_DEV_PASSWORD 不夠(後端 AGENTS.md 與 deploy/BOOTSTRAP.md 仍寫舊流程,以 Makefile 為準)。報告預設寫到 .apifox/reports,隔離流程則在結束時印出日誌目錄。密碼只從環境變數或 0600 的私有 fixture 檔來,不可放雲端或版控。
除了 CLI success,還要看報告中的斷言數大於零、場景命中正確版本/租戶,並保留 4xx 關鍵錯誤驗證。Apifox 內建的響應結構校驗目前不可信,契約守門仍以 tests/contract/openapi_* 為準。沒有前置授權時,誠實回報「場景已建、斷言未驗」,不把 ci-local 綠當 Apifox 綠。
預期結果、常見錯誤與相關文件
預期結果: 每個改動都有最小 unit/contract,資料庫改動有 integration/RLS,跨服務流程有 E2E,對外端點有 Apifox,部署行為另有版本與環境證據。
常見錯誤:只看單元綠、以為 ci-core 已涵蓋 tests/unit/...、把 skipped 當 passed(包括缺後端 sibling 時的前端 contract 測試)、用 mock 驗登入、只設 NEXUS_DEV_PASSWORD 就跑 make apifox-test、Apifox 斷言數 0、新增路由或 migration 卻漏改封閉清單、把前端 pipeline 綠當測試綠、共用 dirty compose、未 rebuild image、或只測錯誤不測真正成功。交付格式與部署差異見測試與交付,異常排查見常見問題。