deep-dives

19 posts

你的架構圖早已是謊言

架構文件從你存檔的那一刻起就開始腐敗。以下說明如何透過程式碼產生的圖表、ADR 與自動化架構測試,讓文件保持誠實。

我在 wiki 裡看過的每一張架構圖都是錯的。不是明顯的錯,而是安靜地、漸進地錯。標示為「Auth」的服務在六個月前就被拆成三個微服務。標示為「sync call」的箭頭現在已經透過 queue 變成 async。標示為「PostgreSQL」的資料庫在某次火災演練中被遷移到別的東西,但沒人更新那個方框。…

你的重試迴路假設第一次請求失敗了。它大概沒有。

超時或當機不代表你的 API 請求遺失了。以下是 idempotency keys 如何讓重試變得安全,以及真正能防止重複的儲存模式。

你的服務在處理 請求到一半時當機了。客戶端看到超時並重試。現在你有兩筆扣款。客戶很生氣。資料庫是一致的。你的商業邏輯不是。 這不是邊緣案例。這是分散式系統的預設行為。網路會丟封包。container 會在請求處理到一半時被 OOM-killed。負載平衡器會對已經抵達後端的請求回傳 502。如果你的 API…

比你的进程更長命的 lock:分散式租約的實際運作原理

記憶體中的mutex會在伺服器重啟後消失。本文說明具有 fencing token 與 TTL 的分散式租約如何防止當機後的重複執行,以及它們仍然會失效的地方。

你的 撐不過 。它也撐不過 OOM、部署推出或節點重啟。程序結束的瞬間,lock 就消失了。如果那把 lock保護的是一個排程任務、資料遷移或領導者選舉,你現在會有兩個程序都認為自己是唯一在執行的那個。 這不是你的mutex有 bug。這是類別錯誤。程序本地的lock無法保護叢集範圍的資源。…

沒有 goroutine、沒有 timer、也沒有背景開銷的斷路器

大多數斷路器函式庫會產生背景執行緒來探測恢復狀態。你並不需要它們。這裡介紹一種由請求驅動的設計,能在不犧牲正確性的前提下,消除所有背景開銷。

我審閱過的每一個 production 級斷路器,最終都會產生一個背景執行緒。它可能是 Go 的 goroutine、Java 的 ,或是 Rust 的 tokio task。工作內容永遠一樣:每隔幾秒醒來一次,檢查下游服務是否已恢復,然後從 OPEN 切換回 CLOSED。…

你的 Web Service 有一條 Graceful Shutdown 路徑。那就是 Bug。

Crash-only software 把每一次失敗都當成 crash,把每一次啟動都當成 recovery。對 Web Service 來說,這意味著刪掉你的 shutdown 邏輯,並設計出能撐過 kill -9 的狀態。

你的 Web Service 有一個 shutdown handler。它會 flush buffer、關閉連線、寫入 checkpoint。你也許測試過一次。在生產環境,它大概一年只在計畫性部署時執行一次。其他時候,你的服務死於 OOM kill、node eviction、斷電,或是部署超時後被 SIGKILL。…

當你不懂 mutant 改了什麼時,如何殺掉一個存活下來的 mutant

Mutation testing 發現了一個 survivor,但你完全不知道這個 mutation 到底做了什麼。這裡有一個逐步方法,讓你在還沒理解 mutant 之前就能寫出正確的 test。

你的 mutation testing 報告充滿了 survivors,其中至少有一個讓你完全摸不著頭緒。 工具說它在第 47 行把 翻成了 ,或是把整個 conditional block 替換成 ,或是 mutate 了一個你根本不知道正在被測試的 string literal。你把 diff…

Auth 程式碼需要 90% mutation coverage。你的 string utils 不需要。

為何在整個 codebase 強制套用單一 mutation score 是錯的,以及如何根據實際風險設定各模組的門檻。

在整個 codebase 強制套用單一的 mutation score,是讓團隊討厭寫測試的絕招。 拿 PIT 或 Stryker 跑一個典型的 repo,你會看到同樣的模式:auth 模組只有 40%,string utilities 衝到 95%,ORM 層則卡在 60 幾趴。本能反應是設一個 70%…

你的測試全過了,Mutation Score 卻只有 40%——Surviving Mutant 到底在跟你說什麼

Code coverage 告訴你很安全,Mutation testing 卻說你的測試大多是擺設。這篇文章告訴你 surviving mutants 如何戳破這個假象,以及你該怎麼補破洞。

你的測試全過了,coverage report 顯示 87%,但 mutation score 只有 40%,還有一半的 mutants 活得好好的。 這個 40% 不代表你的程式壞了,它代表你的測試壞了。Coverage 衡量的是「測試執行時跑過哪些行」;Mutation testing…

Rust 的 Mutation Testing 確實有效,但編譯時間會讓你痛苦

cargo-mutants 能找出那些只會假裝驗證程式碼的測試。以下是 mutation testing 在 Rust 中的運作原理、它能抓到什麼問題,以及編譯時間的成本是否值得。

你已經達到 100% 的行覆蓋率。每個分支都被執行過,每個函式都被呼叫過。然後有人把你的計價邏輯裡的 改成 ,跑了一下測試,全部通過。 這不是理論上的問題。這就是你的測試執行了程式碼,卻沒有真正驗證行為時會發生的事。Coverage 衡量的是哪些行被執行過,而不是哪些輸出被檢查過。Mutation testing…

Mutation testing 跑四小時,團隊到底怎麼在 CI 裡用它?

大多數團隊不會在每次 commit 都跑完整的 mutation testing。這裡告訴你工程團隊如何實際把 mutation testing 整合進 CI,又不會搞爛 build pipeline。

如果你的 mutation testing suite 要跑四小時,恭喜你。你證實了大家早就猜到的事:你的測試有漏洞。 你不會在每次 push 都跑這個。沒有團隊這樣做。問題不是你能不能負擔每次 commit 四小時,而是你能不能承受程式碼測試通過了,卻根本沒有驗證任何東西。 Code coverage…

單元測試全綠,資料卻憑空消失

模擬資料庫測試只能驗證 SQL 語法,無法確認資料列是否能撐過當機、並發寫入或 schema 不符。以下是真的測試資料持久化的方法。

如果你在測試中使用模擬資料庫,你其實只是在驗證 repository 層是否呼叫了正確的方法。你並沒有測試資料能否在當機後存活、唯一約束是否真的阻止重複資料,或者transaction 失敗時是否會回滾。 這個差別很重要。模擬的 只會回傳你預設的結果。真正的…

不用淹沒在 mock action 裡也能測試 Redux

把每個 Redux action 都 mock 掉,只會讓你的測試變成變更日誌驗證器。以下介紹如何改用真實的狀態轉換來測試你的 store。

如果你曾經寫過一個測試,去驗證 是否被以完全正確的 payload 形狀呼叫,那你其實寫了一個「只要有人重新命名常數就會壞掉」的測試。 這不是測試你的狀態邏輯。這是在測試你的手指有沒有打對字串。 Redux 測試教學通常從 Jest mock 開始:監聽 ,斷言 action creator 被呼叫了,斷言 type…

跑 100 次測試是騙人的:如何真正決定 Property-Based Test 的執行次數

Property-based testing 預設跑 100 個範例是一種社交妥協,而非統計策略。以下是根據你的信心需求與 CI 預算來選擇執行次數的方法。

如果你用預設的 100 個範例來跑 property-based tests,那你等於同時承受了兩種壞處。你的 CI 比實際需要還慢,而且那些真正重要的 bug 你還是抓不到。 這個數字沒有什麼魔力。大多數函式庫,包括 Hypothesis 在內,預設設成 100…

Rust 的 Property-Based 測試:找出單元測試漏掉的 Bug

範例導向的測試只涵蓋你想得到的輸入。Property-based 測試會產生隨機資料、檢查不變條件,並將失敗縮減到最小的反例。

你寫了一個 函式。你用 和 測試它。通過了。你發布出去。 有個使用者傳入了一個單元素的 slice。你的函式把它遺漏了。他們開了一個 issue。你盯著測試檔案,納悶自己怎麼會漏掉這麼明顯的東西。 你之所以漏掉,是因為範例導向的測試只能抓到你預期中的 bug。測試套件裡的每一個…

你的單元測試過了。你的正式環境程式碼還是壞的。

程式碼覆蓋率指標製造了虛假的安全感。以下是單元測試為何總是漏掉那些讓你睡不著的 bug,以及你該改測什麼。

你有 90% 的程式碼覆蓋率,凌晨兩點還是被 on-call 警報吵醒。 單元測試全過了。CI 也是綠燈。bug 還是進了正式環境。覆蓋率沒有說謊,但也沒有說出真相。它只衡量了哪些行被執行過,沒衡量哪些行為真的被驗證過。…

Rust Runtime Contracts 在 Release Build 中可以零成本,但編譯器不會幫你做到

Rust 會自動剝除 debug assertions,但真正的 design-by-contract 需要的遠不止 debug_assert!。以下說明如何建立零成本的 runtime contracts,讓它們從你的 release binary 中完全消失。

Rust 可以在開發階段強制執行 runtime contracts,並在 release build 中將它們完全抹除。但書在於,這門語言並未將 contracts 視為 first-class concept。你拿到了積木,但得自己動手組裝。 是最顯而易見的起點。它在 debug build 中執行,在…

零個、一個,還是十二個:一個 production function 到底需要多少 assertion

開發者要不是把 assertion 灑滿整份程式碼,就是完全避開它們。這裡有一個決策框架,能幫你區分出有用的 invariant 與會導致 production crash 的觸發條件。

大多數 production codebase 可以分成兩大陣營。A 陣營把 當成裝飾調味料,每隔幾行就撒上一點,直到 function 讀起來像偏執律師寫的法律合約。B 陣營把 assertion 當成只在開發階段用的輔助輪,在 build 時全部拿掉,然後祈禱程式在 production…

你的驗證層比業務邏輯還龐大

手動驗證讓 codebase 膨脹,卻還是漏掉邊界情況。以下說明如何透過宣告式 schema 來 enforce runtime contracts,讓它們不干擾你的開發流程。

每次你的 API 收到請求,你就會驗證它。每次函式收到來自外部系統的參數,你就會檢查它。如果用手動方式處理,單一 endpoint 累積的驗證程式碼可能比業務邏輯還多。 這就是 runtime contracts 的隱藏代價。你需要它們,因為 type system 會說謊:透過 HTTP 傳來的…

TypeScript strictNullChecks 是編譯時守門員,不是執行時防護罩

strict 模式抓得到你寫出來的 null,卻抓不到執行時從 API、DOM 查詢和 JSON.parse 傳進來的 null。型別系統到此為止,接下來就是你的防線。

你在 裡把 設成 ,修掉了每一條紅色波浪線,信心滿滿地發佈到正式環境,以為 和 已經不再是問題。 結果後端回應改了格式、DOM 查詢什麼都沒拿到, 在 TypeScript 認定絕對安全的那行程式碼上拋出了 。到底發生了什麼事? TypeScript 的 strict null checks…