雲端環境中的測試簡介
在 Google Cloud 上建置雲端原生應用程式需要分層的測試策略,因為受測系統不再是單一執行檔。一個典型的 Cloud Run 服務可能依賴 Firestore、Pub/Sub、Spanner、Secret Manager、Identity-Aware Proxy,以及三四個下游微服務。Professional Cloud Developer(PCD)考試期望你懂得如何將測試左移,讓大部分缺陷在本地或 CI 階段就被攔截,而不是在 staging,更不該到 production 才爆出來。
PCD 的考試藍圖把「為可測試性而設計(designing for testability)」列為一級開發者技能。這代表你寫的程式要能注入用戶端(如此才能換成模擬器)、整合測試要透過 seed state 保持確定性,並且負載與混沌測試要納入發佈流程。本章節走過完整的 Google Cloud 測試工具箱:Firestore、Spanner、Pub/Sub、Bigtable 的模擬器;Firebase Emulator Suite 用於 client-server 測試;Cloud Build 編排平行測試分流;Pact 契約測試;GKE 上的 k6 與 Locust 負載測試;以及 DLP 去識別化用於安全的測試資料。
PCD 考試常出現的選項是在某個測試層次裡,要在 mock、模擬器、真實服務之間選擇。原則:單元測試用 mock,整合測試對有狀態服務(Firestore、Spanner、Pub/Sub、Bigtable、Datastore)使用 gcloud emulators,端對端測試則跑在真實的 Google Cloud 專案,通常是針對每個 pull request 啟動的暫時專案。
Google Cloud 工作負載的測試金字塔
經典的 Mike Cohn 測試金字塔在雲端原生開發依然成立,只是比例會偏移,因為雲端依賴的啟動與拆除代價昂貴。
單元測試(佔比 70%)
單元測試專注在單一函式或類別,所有外部協作對象都換成 mock 或 fake。在 Google Cloud,這通常代表注入假的 firestore.Client、mock 掉 google-cloud-pubsub 的 PublisherClient,或用 httptest.NewServer(Go)/ nock(Node)模擬下游 HTTP API。整套單元測試應在 10 秒內跑完且不需要網路。它們應該(也值得)在每次存檔時透過 watcher 自動執行。
整合測試(佔比 20%)
整合測試驗證你的程式碼跟有狀態依賴之間的線上協定是否吻合。與其付錢開真實 Spanner 或 Firestore,你跑一個本地 emulator,它實作相同的 gRPC API。模擬器具有確定性、快速(每個測試 10–50 ms)且免費。整套整合測試應在 5 分鐘內跑完,才能用來擋 PR 合併。
端對端測試(佔比 10%)
E2E 測試跑在真實 Google Cloud 專案上。它們慢、容易 flaky、又貴,所以要保持精簡:happy path 的 smoke test,加上過去壞過的回歸測試。常見模式是針對每個 pull request 開一個暫時環境(見第 9 節),再用幾隻 Playwright 或 Cypress 跑過去。
健康的 PCD 風格測試套件大致以測試數量來看是 70 / 20 / 10 比例的單元 / 整合 / E2E,但成本比例完全顛倒:E2E 雖然數量最少,卻吃掉 CI 大部分的分鐘數。
Firebase Emulator Suite:Firestore、Realtime DB、Auth
Firebase Emulator Suite 是 Google 為 client-server 應用程式提供的本地環境中最高保真的一套。它把 Firestore、Realtime Database、Cloud Functions、Authentication、Cloud Storage for Firebase、Pub/Sub、Eventarc、Hosting 的模擬器全部打包進單一 CLI。
安裝與啟動
透過 Firebase CLI 安裝,並在 firebase.json 設定要啟動哪些模擬器:
npm install -g firebase-tools
firebase init emulators
firebase emulators:start --only firestore,auth,pubsub
這套工具用固定 port 暴露每個模擬器(Firestore 8080、Auth 9099、Realtime DB 9000、Pub/Sub 8085 為預設值),並在 http://localhost:4000 提供 Emulator UI,可以即時檢視文件、安全規則執行軌跡,以及驗證使用者。
安全規則測試
Firestore emulator 是唯一能跑 @firebase/rules-unit-testing 函式庫的環境,可以針對特定已登入使用者,斷言你的 firestore.rules 是否允許或拒絕特定操作。這也是測試多租戶安全規則卻不會碰到 production 資料的標準作法。
CI 整合
CI 中使用 firebase emulators:exec "npm test",它會啟動模擬器、執行你的測試指令,並以正確的 exit code 收尾。模擬器支援 --import 與 --export-on-exit 配對使用,可以把已 seed 的資料集快照下來、commit 進 git,讓每次測試都從相同基準起跑。
Emulator fidelity(模擬器保真度)指的是模擬器忠實重現 production API 表面的百分比。Firestore 模擬器約 99% 保真(包含 transactions、queries、安全規則)。Cloud Functions 模擬器約 95%。Auth 模擬器不會用 production 私鑰簽 token,所以任何要拿 Google JWKS 驗簽的程式都會失敗,這時要改用模擬器自己的本地公鑰端點。
透過 gcloud 使用 Spanner、Pub/Sub、Bigtable 模擬器
Firebase 之外,Google Cloud 透過 gcloud CLI 另外提供一系列模擬器。這些只有命令列、沒有 UI,但已涵蓋後端服務最常用的重量級依賴。
Spanner 模擬器
Spanner 模擬器在本地以單節點模式啟動,兩秒內就緒。它支援完整的 Cloud Spanner gRPC API、GoogleSQL 與 PostgreSQL 兩種方言、secondary index、foreign key、change stream。它不支援 backup/restore、IAM 與 metadata API。
gcloud emulators spanner start
$(gcloud emulators spanner env-init)
gcloud spanner instances create test-instance --config=emulator-config --description="Test" --nodes=1
env-init 會 export SPANNER_EMULATOR_HOST=localhost:9010,任何 Spanner client library 都會自動偵測到模擬器並跳過驗證流程。這是在 Cloud Build 跑 Spanner 端對端測試最乾淨的方式,完全不用開真實 instance。
Pub/Sub 模擬器
Pub/Sub 模擬器實作 publish、subscribe、ack、modify-ack-deadline、seek 這些 API。它不實作 IAM、跨專案的 dead-letter forwarding、跨地區的訊息排序,但本地開發已綽綽有餘。
gcloud components install pubsub-emulator
gcloud emulators pubsub start --project=test-project --host-port=localhost:8085
$(gcloud emulators pubsub env-init)
Bigtable 模擬器
Bigtable 模擬器(gcloud beta emulators bigtable start)支援 column family、row filter、mutation、read。它不支援 replication、instance/cluster 管理 API、app profile,所以任何受測程式只要用到這些,在模擬器執行時都應該用 feature flag 包住。
這三套模擬器都靠環境變數來自我宣告(SPANNER_EMULATOR_HOST、PUBSUB_EMULATOR_HOST、BIGTABLE_EMULATOR_HOST)。如果你忘了在 production 部署時把這些 unset 掉,你的服務會默默嘗試連 localhost:9010 然後直接 crash。Cloud Build 設定裡務必把 emulator 環境變數鎖在 TESTING=true flag 之後。
用 Cloud Build 跑 CI 測試
Cloud Build 是 Google 的代管 CI 服務,也是 PCD 風格測試流程的預設執行場域。一個 cloudbuild.yaml 是一組 step 的清單,每個 step 是一個 container,依序執行(或用 waitFor 關鍵字平行執行)。
最小測試流程範例
steps:
- id: lint
name: 'node:20'
entrypoint: 'npm'
args: ['run', 'lint']
- id: unit
name: 'node:20'
entrypoint: 'npm'
args: ['test', '--', '--coverage']
- id: integration
name: 'gcr.io/cloud-builders/gcloud'
entrypoint: 'bash'
args:
- -c
- |
gcloud emulators firestore start --host-port=0.0.0.0:8080 &
sleep 3
FIRESTORE_EMULATOR_HOST=localhost:8080 npm run test:integration
options:
machineType: 'E2_HIGHCPU_8'
logging: CLOUD_LOGGING_ONLY
把 machineType 升到 E2_HIGHCPU_8 是關鍵:預設 1-vCPU 的 builder 同時跑模擬器與 Node.js 測試太小,會撞到 timeout。
Build Trigger 與分支政策
設定一個 Cloud Build trigger,讓它在每次 PR 打進 main 時觸發,並加上 path filter 跳過只動文件的 PR。用替換變數($_DEPLOY_ENV、$SHORT_SHA)來驅動下游的暫時環境命名。trigger 的「Required status check」可以一對一對應到 GitHub 分支保護,紅燈 build 直接擋住 merge 按鈕。
Production 的人工核可
production-bound 的 build 要打開 trigger 上的人工核可。build 會在 deploy step 暫停,直到擁有 roles/cloudbuild.approver 角色的人到 Console 按 Approve,這把「測試跑完」與「上 production」乾淨地分開。
平行執行測試
測試一條線跑下去無法擴展。30 分鐘的測試套件足以毀掉開發速度,所以 PCD 等級的 pipeline 會把測試 fan out 到多個 worker 上。
依測試檔案 sharding
最簡單的模式是檔案層級 sharding:把測試檔分成 N 組,並用 waitFor: ['-'](表示「不依賴任何 step,立即開跑」)跑 N 個 Cloud Build step。大部分測試框架原生支援 — Jest 有 --shard=1/4、Go test 有 -run regex、Vitest 有 --shard。
- id: test-shard-1
name: 'node:20'
entrypoint: 'npx'
args: ['jest', '--shard=1/4']
waitFor: ['-']
- id: test-shard-2
name: 'node:20'
entrypoint: 'npx'
args: ['jest', '--shard=2/4']
waitFor: ['-']
Cloud Build Private Pool
當平行度超過預設的 10 個同時 build,改用 private worker pool。private pool 可以保留多達 30 個高 CPU worker,並把它們放進 VPC 裡,讓 build 能直接連到私有的 Cloud SQL 或 AlloyDB instance。
測試結果彙整
讓每個 shard 把 JUnit XML 寫到 Cloud Storage bucket,key 用 ${BUILD_ID}/shard-${i}.xml。最後一個 step 把它們合併、透過 gh pr comment 在 PR 上留言,給開發者一行 pass/fail 結論和完整報告的連結。
Cloud Build 是按每個 vCPU 的 build-minute 計費。四個 8-vCPU shard 各跑 5 分鐘,跟一個 8-vCPU 順序跑 20 分鐘的價錢一模一樣 — 但工程師只要等 1/4 的時間。從工程師時間來看,平行度其實是免費的。
用 Pact 進行契約測試
微服務各自演進時,整合測試還不夠,因為每個服務只擁有契約的一半。Pact 是消費者驅動契約測試的開源工具,就是用來補這個缺口。
Pact 怎麼運作
- Consumer 服務寫 Pact 測試,描述自己會送的請求和期望收到的回應。跑這些測試會產出一份 pact file(一份 JSON 契約)。
- 這份 pact file 發佈到 Pact Broker(Pactflow,或用 Cloud Run + Cloud SQL 自架)。
- Provider 服務跑
pact-verifier去驗證自己的真實實作;如果有任何 consumer 的期望被打破,provider 的 CI 就會失敗。 can-i-deployCLI 在 Cloud Build 把任何一邊推上 production 之前,先檢查這對 consumer/provider 的契約版本是否已互驗成功。
把 Pact 串進 Cloud Build
- id: pact-publish
name: 'pactfoundation/pact-cli'
args:
- publish
- pacts/
- --consumer-app-version=$SHORT_SHA
- --broker-base-url=$_PACT_BROKER_URL
- id: pact-can-i-deploy
name: 'pactfoundation/pact-cli'
args:
- broker
- can-i-deploy
- --pacticipant=checkout-service
- --version=$SHORT_SHA
- --to-environment=production
can-i-deploy step 在任何 provider 還沒驗證新 pact 時就會 exit non-zero,自動擋下部署。
契約測試是消費者驅動:consumer 主張自己需要什麼,provider 證明自己能交付。這跟 OpenAPI spec-first 測試正好相反,後者是 provider 訂規矩、consumer 配合。PCD 題目中只要提到「兩個團隊各自獨立演進 API,且不能互相打破」,答案幾乎必然是 Pact。
Cloud Run 與 GKE 上的混沌測試
混沌工程刻意注入故障(殺實例、注入延遲、丟封包),來驗證 retry、circuit breaker、fallback 這些韌性機制是不是真的有用。Google Cloud 沒有代管的混沌服務,但可以用原生元件自己拼出來。
Cloud Run 上的混沌
Cloud Run 的 revision 是不可變的,所以不能直接殺 instance。取而代之的做法是:
- 流量分割:把 50% 流量導到刻意壞掉的 revision(例如每五個請求 throw 一次),觀察 client 的 retry 能不能成功。
- 延遲注入:部署一個 sidecar 或在 handler 外包一層 middleware,1% 的請求 sleep 5 秒,再驗證上游負載平衡器的
connectTimeoutMs: 2000是否如預期觸發。 - 依賴移除:把 runtime service account 的
roles/datastore.user撤掉 60 秒,確認服務會優雅降級而不是直接吐 500。
GKE 上的混沌
GKE 上用 Chaos Mesh(CNCF 專案),透過一組 CRD 安裝,可以跑 PodChaos(亂殺 pod)、NetworkChaos(在兩個 namespace 之間注入 200 ms 延遲)、HTTPChaos(對 10% 符合路徑的請求回 503)等實驗。把實驗排在離峰時段自動執行,並對 SLO 燃燒設警報。
Game Day
每季辦一次 Game Day:宣布混沌時段、注入一個真實故障(例如刪掉 staging Spanner instance),讓 on-call 團隊實際演練復原 runbook。PCD 把這視為「為可靠性而開發」的一部分。
在 GKE 上用 k6 與 Locust 做負載測試
負載測試回答「系統能不能撐過流量尖峰 X?」這個問題 — 而 production 永遠不該是第一個被問的對象。
GKE 上的 k6
k6 是 Go 寫的負載測試工具,腳本用 JavaScript。推薦做法是把 k6-operator 部署到專屬 GKE node pool,用 TestRun CRD 跑分散式負載測試:
apiVersion: k6.io/v1alpha1
kind: TestRun
metadata:
name: checkout-stress
spec:
parallelism: 50
script:
configMap:
name: k6-test
file: checkout.js
parallelism: 50 會起 50 個 pod,每個 pod 跑一個 k6 腳本實例,相當於協調 50 個 worker 一起壓。k6 用 Prometheus 格式輸出 metric,Cloud Managed Service for Prometheus 會抓取並顯示到 Cloud Monitoring 儀表板。
GKE 上的 Locust
Locust 用 Python 腳本模型,原生支援 leader-worker 架構。把 master 用單一 Deployment + Service 起來,再把 worker Deployment 擴展到 N 個 pod。master 在 8089 port 的 Web UI 會顯示即時 RPS 圖與 percentile 延遲。
怎麼挑目標 RPS
有意義的負載測試應該爬到目前尖峰流量的 2 倍並維持 30 分鐘,再階梯式拉到尖峰的 5 倍維持 10 分鐘,找出懸崖。在過程中要抓 Cloud Run revision concurrency、Spanner CPU、Pub/Sub backlog,才能定位瓶頸。
絕對不要沒先用 gcloud compute project-info describe 拉高相關配額、也沒跟 Google 知會,就直接對 Cloud Run 服務或 Cloud Load Balancer 開負載測試。一個 10 萬 RPS 的負載測試會輕易撞破預設的單專案配額(例如 Cloud Run 預設 30k concurrent request),結果是你的專案會在 API 邊緣被 rate-limit — 看起來跟真實服務中斷一模一樣,還會連累不相關的服務。
每個 PR 的暫時環境
最頂級的開發者體驗是:開一個 PR、5 分鐘內拿到一條獨立 URL、點來點去、留個 comment、合進去、看著環境自我銷毀。PCD 把這個 pattern 叫做preview environments。
Cloud Run Preview 模式
Cloud Run 是最簡單的目標,因為它的每個 revision 本來就有唯一 URL。流程:
- PR 打開 → Cloud Build trigger 觸發。
- Build 把 image 用
$SHORT_SHA推到 Artifact Registry。 gcloud run deploy pr-${PR_NUMBER}建立 PR 專屬的 service。- Build 在 PR 上 comment 出 URL。
- PR close / merge 時,另一個 trigger 跑
gcloud run services delete pr-${PR_NUMBER}清掉。
每個 PR 一個資料庫
有狀態服務的做法通常是共用一個 Spanner instance,但每個 PR 開自己的資料庫(pr-${PR_NUMBER})。Spanner 的資料庫層級隔離不會多花錢(只算 node、不算 database),可以拿到完整的 schema 隔離。
Firestore 則用命名資料庫(projects/PROJECT/databases/pr-${PR_NUMBER})— Firestore 從 2024 年起就支援單一專案多資料庫。
生命週期管理
每個資源(Cloud Run service、Spanner database、Firestore database、Pub/Sub topic)都用 PR 編號打 tag。一個排程的 Cloud Run Job 每晚跑一次,把對應 PR 已關閉超過 24 小時的資源全部回收掉。這樣可以避免「上千個殭屍環境」的問題。
用 DLP 去識別化做測試資料
Production 資料是保真度最高的測試資料 — 但直接拿來用就是合規災難。PCD 認可的解法是 Cloud Data Loss Prevention (DLP) 去識別化。
去識別化管線
一個排程的 Dataflow job 從 production BigQuery table 讀資料,把每一列丟給 DLP 的 deidentify API,套用以下轉換:
- Redact:直接識別資訊(姓名、地址)透過
REPLACE_WITH_INFO_TYPE替換。 - Tokenize:類識別資訊(帳號 ID、電子郵件)透過 format-preserving encryption (FPE) 加密,讓跨表的 referential integrity 維持。
- Generalize:日期一律歸納到月或年的 bucket。
- Bucketize:數值離群值聚合,避免極端值反向識別。
輸出寫到一個獨立的 *-test dataset,測試環境就從那裡讀。
FPE 為什麼重要
如果你只是把帳號 ID hash 掉,每一筆對該帳號的引用都會變成不同的 opaque blob,JOIN 直接壞掉。FPE 產出的是 deterministic、保留格式的 token(16 位信用卡卡號還是 16 位、依然通過 Luhn 檢查)— JOIN 依然能用,靠跨表關聯的測試案例也能通過。
合成資料作為後備
對還沒有 production 資料的新產品,用 Faker(Python)或 @faker-js/faker(Node)這類函式庫產合成資料。把隨機種子鎖死,這樣每次 CI 都拿到一樣的資料集。
即便是去識別化過的資料,也仍要遵守你的資料處理政策。許多合規規範(HIPAA Safe Harbor、GDPR Art. 26)要求在去識別化資料離開 production VPC 邊界之前,先做一份書面的重新識別風險評估。這份評估要在管線建好之前做,不是事後補。
白話文解釋
類比 1:飛行模擬器
用模擬器測試就像飛行員用飛行模擬器訓練。你可以練起飛、降落、應對暴風雨,完全不用離開地面、也不會弄壞真的飛機。Spanner 模擬器 2 秒就啟動、成本 $0;真實的 Spanner 要 5 分鐘、每節點 $0.90/小時。沒理由不先用模擬器。
類比 2:消防演習
負載測試就像摩天大樓的消防演習。你想知道樓梯能不能撐住所有人同時離開。k6 在 50 個 GKE pod 上跑,等於同時製造 5 萬個情緒激動的使用者,全部同一秒撞向你的 Cloud Run 服務。如果警報響起、大家有秩序離開(自動擴展啟動、p99 維持在 500ms 以下),那很好。如果半棟樓的人卡在三樓(Spanner CPU 飆到 100%),你就知道在真正失火前該先去拓寬哪一條樓梯。
類比 3:撞擊測試假人
混沌工程是軟體界的撞擊測試假人。汽車製造商不會等真的車禍發生才知道安全帶不夠強 — 他們在實驗室裡用 60 mph 把假人砸進牆裡。GKE 上的 Chaos Mesh 在做一樣的事:依照你訂的排程,刻意殺 pod、注入 200 ms 網路延遲、對 10% 的請求回 503。等到凌晨 3 點真的出包,你的 retry 邏輯和 circuit breaker 早就被砸過上千次牆,早已是熟練的肌肉記憶。
常見問題
Q1:我該怎麼在本地測試 Cloud Functions、不要部署上去?
用 Functions Framework(Node 用 @google-cloud/functions-framework、Python 用 functions-framework)。它會把你的函式當成本地 HTTP server 跑在 8080 port,接受 HTTP 請求或在 / 收模擬的 CloudEvent。搭配 Firebase Emulator Suite 還能在本地串接 Pub/Sub trigger:Pub/Sub 模擬器把事件推給 Functions Framework 的 URL,行為跟 production Eventarc 一模一樣。
Q2:GCP 上做負載測試該選 k6、Locust、JMeter 還是 Artillery?
Google 沒有提供代管負載測試服務,所以選擇權完全在你手上。k6 是現在的預設首選:JavaScript 腳本、原生 Kubernetes operator、Prometheus metric、單一輕巧的 Go binary。Locust 適合 Python 為主、喜歡 leader-worker UI 的團隊。JMeter 只在你還在跑 SOAP/JMS 的舊系統時才比較適合。Artillery 處理小型 Node 工作負載 OK。PCD 考試比較常點名 k6 與 Locust。
Q3:我能在本地跑整個 Firebase 專案嗎?
可以 — firebase emulators:start 會啟動你在 firebase.json 宣告的每一個模擬器,包括 Hosting、Functions、Firestore、Auth、Storage。4000 port 上的 Emulator UI 提供統一儀表板。你甚至可以讓真實的 React 或 Flutter App 透過 client SDK(用 connectFirestoreEmulator、connectAuthEmulator 等)連到 emulator suite,整支 App 在本地就能跑完整端對端流程。
Q4:我已經有 OpenAPI spec 了,還需要 Pact 嗎?
需要,兩者解決的問題不同。OpenAPI 描述provider 自稱會做什麼,是單方面的。Pact 描述每個 consumer 實際依賴什麼。一個 provider 可能完全符合 OpenAPI 規格,卻還是把 consumer 弄壞 — 例如把過去回 0 的欄位改回 null。Pact 抓得到,因為 consumer 的契約裡寫的是 0,不是「任何整數」。
Q5:怎麼避免暫時 PR 環境把 GCP 帳單炸開?
三個控制點。第一,TTL:每個資源用 PR 編號打 tag,跑每晚清理 job。第二,資源規格:gcloud run deploy 加上 --cpu=1 --memory=512Mi --min-instances=0 --max-instances=2,閒置 preview 一毛錢都不用花。第三,共用依賴:一個 Spanner instance 配每個 PR 一個 database,遠比每個 PR 自己一個 instance 便宜得多。
Q6:Firestore 模擬器和 Datastore 模擬器有什麼差別?
Firestore 模擬器實作新版 Firestore API(collection、document、安全規則、query)。Datastore 模擬器實作舊版 Cloud Datastore API。如果你的 App 用 @google-cloud/firestore 或 firebase-admin,要選 Firestore 模擬器。如果用 @google-cloud/datastore,要選 Datastore 模擬器。兩者不可互換,雖然 Firestore in Datastore mode 在 production 是同一個後端。