單元測試全綠,端到端一路紅。每個人都完成了自己那一段,只是沒有人負責那條路。
昨天說到,三張 Ticket 各自完工,看板一片綠,來到整合日。
照劇本,今天只是「把三段接起來」:前端的退款按鈕、後端的退款 API、金流商的串接,下午就能在測試環境看到第一筆線上退款。PM 已經在想 Demo 要約哪一天。
上午十點,前端工程師把畫面接上後端。三分鐘後,出現第一個問題。
前端等的是一個是非題:成功,或失敗。畫面邏輯很單純——成功就顯示「退款成功」,失敗就顯示錯誤訊息。後端回的卻是一個狀態字串:處理中、已退款、退款失敗。前端收到一個看不懂的答案,畫面卡在轉圈。
兩邊把程式碼攤開來對,發現不只格式不同,連語意都對不上。前端以為按下按鈕就會直接拿到結果;後端的「已退款」,指的是它呼叫金流商的退款 API 收到 HTTP 200、把訂單狀態改掉的那一刻。
「你們當初怎麼約的?」PM 問。
當初沒有約。三張 Ticket 是同一天開的,各寫各的。昨天那場被「先做再說」擋掉的對齊會議,是唯一一次接近「約」的機會。
中午前,兩邊各退一步,加了一層轉換,欄位勉強對上。下午跑第一次端到端(End-to-End)測試:按下退款、狀態改變、畫面更新——一路紅。
追下去,追出整合日最大的一個洞。金流商的介接文件寫得清楚:退款結果會在稍後用一個回呼通知(webhook)送回來,商家要準備一個端點去接。
「所以 webhook 誰串了?」
安靜。
負責金流串接的工程師以為那歸後端——通知進來要改訂單狀態,訂單是後端管的。後端工程師以為那歸金流串接——那是金流商發過來的東西,當然是串金流的人負責。前端工程師是今天第一次聽到 webhook 這個詞。
三個人各自的單元測試,全部是綠的。三個人抬起頭,說了同一句話:
「我這邊是好的。」
三句都是真話。而整套系統不能用。
至於那個通知裡裝的到底是什麼、跟後端已經改成「已退款」的狀態是什麼關係——當下沒有人多想。先把線接通再說。
事後回看,當時每個人的理解都很合理。
把功能拆成前端、後端、金流三張 Ticket,是分工明確;每個人把自己的 Ticket 做完、單元測試寫好跑綠,是專業負責;整合時有點小狀況很正常,軟體不就是這樣,接一接就好;至於別人那段長什麼樣子,尊重專業,不指手畫腳。
每一句單獨看都有道理。合在一起,剛好讓一件事變得沒有人看見:
前端的「完成」=按鈕按下去,畫面會動
後端的「完成」=API 呼叫成功,狀態有改
金流的「完成」=退款請求送得出去
整套系統的「完成」=客戶按下退款,錢退回去,狀態對得上
前三行加起來,湊不出第四行
看板上有三張卡,每張都拉到了 Done。從按下按鈕到錢退回去的那條路沒有卡,所以它永遠不會 Done,也永遠不會 Blocked——它根本不存在於任何人的管理視野裡。
「大家都完成了」和「系統能用」是兩個命題。把第一個當成第二個,前提是交界的部分有人做對了。而交界,正是這次沒有人認領的東西。
今天缺席的工程責任有兩個。一個比較有名,一個更要命。
有名的那個叫 Interface Contract。前端等是非題、後端回狀態字串,這不是誰寫錯了,是兩個人對著同一個交界各自想像,而想像從來沒有被放到同一張紙上核對過。契約不需要厚:欄位、型別、每個值的語意、錯誤長什麼樣,幾行就夠。重點不是格式,是它必須存在於兩顆腦袋之外,讓雙方對著同一份約定開工,而不是對著「對彼此的猜測」開工。
更要命的那個,叫端到端的 Responsibility。契約回答的是「交界長什麼樣」,但還有一個更基本的問題:這條路整條通不通,誰負責?webhook 就是這一題的照妖鏡——它不屬於前端、不屬於後端、也不完全屬於金流串接,於是三方都有充分的理由認為不歸自己。每一段都有主人,交界與全程沒有主人。
這兩個責任,在兩套方法裡原本都有位置。Waterfall 把介面當成設計階段的產物:Interface Specification 先定,整合有計畫、有時程,理論上不會走到整合日才發現兩邊講不同的語言。敏捷把契約變輕、變早,但從來沒有一條規則叫「不用約」——Working Software 的前提是軟體要能 work,而一條由三段想像拼起來的路,work 不起來。
換句話說,又是那個熟悉的結構:兩套方法都有答案,這個團隊兩套都沒用,用的是第三套——
「大家都很專業,應該不會有問題。」
這個系列的老讀者應該有既視感。Day 09 講過一模一樣的病:介面沒定義沒關係,他們兩個很熟。
那時候的劇情是:兩位老戰友,一句「欄位跟上次一樣,多一個 status」,沒有文件,一次就通。介面契約不是不存在,是存在於兩個人的交情裡。
這個團隊過去也是這樣活的。資深工程師在的時候,跨模組的約定他會順手處理掉——有時是走過去講三句話,有時甚至不用講,因為另一端多半就是照他上次的做法寫的。而且他被金流商炸過,開工第二天就會問出那句:「webhook 誰接?」
不是因為流程要求。是因為他的傷疤要求。
但他被借調去救另一個專案,整個第三部都不在場。這次的三個人,沒有誰跟誰熟到能腦補對方的欄位,也沒有人身上有金流商留下的傷疤。靠默契運作的介面,前提是默契真的存在;默契不在場,介面就只剩下「沒有定義」這個事實本身。
於是 Day 09 賒的帳,寄到了 Day 17。大神在的時候,這張帳單每次都被他默默代墊,組織從來沒看過它長什麼樣子。今天是第一次,全額,寄到了整個團隊桌上。
老規矩。Day 15 說過這個案子的帳單會稍後寄達,Day 16 說看板越綠、債越高。今天,第一期帳單到了:
Scope □ 功能一項都沒少做——問題正是每項都「做完」了
Time ■ 「接一接就好」的下午,變成看不到底的聯調週
Cost □
Quality □ 下一期帳單,已寄出
Risk □ 前兩天堆的未爆彈,今天爆了第一顆
人 ■ 三個「完成」的人,一起開始加班 ← 這格回來了
值得注意的是,這次的吸收方式跟前兩部不一樣。大神在的時候,代價被他一個人吸掉,組織看不見,所以流程「看起來沒問題」。這次沒有人有能力代墊,代價第一次以原始面貌兌現:時程當場爆掉、三個人開始加班。
難看,但誠實。假敏捷的成本從來不會消失;大神不在,它終於用市價記帳。
今天的 Artifact 是一張表,在任何多人分工的功能動工前填。它不是介面文件的替代品,它是「這條路有沒有主人」的照妖鏡:
□ 這條流程從頭到尾分幾段?
(畫出來:畫面 → API → 金流 → 通知回來 → 狀態 → 畫面)
□ 每一段的負責人是誰?
(每段都要落到具體的人;「應該是他們吧」不算答案)
□ 每個交界的輸入輸出長什麼樣?
(欄位、型別、語意;兩端各自寫下再核對,對得上才算約好)
□ 整條路通不通,誰負責?
(每段都有人負責,不代表整條路有人負責——這行必須另外填人)
填的時候,最容易卡住的是第一行和最後一行。第一行畫不全,代表有些段——例如那個通知——根本不在大家的地圖上;最後一行填不出來,代表你們正走在這篇文章的劇情裡。
三句「我這邊是好的」加起來,不等於「整條路是通的」——因為交界,從來不在任何人的「這邊」裡。
聯調又熬了幾天,轉換層上了、webhook 端點也趕工立了起來,線勉強接通。就在「已退款」三個字準備放上畫面的時候,有人盯著金流文件,問出了一個所有人都以為早有答案的問題:
金流回 200,到底算不算退款成功?
明天來看這個做到一半,才第一次被問出來的問題。