我曾說我對測試一無所知,但我的倉庫裡有342個測試
一位沒有程式設計背景的“氛圍編碼者”在AI輔助下完成專案後,驚訝地發現AI自動編寫了342個自動化測試。文章深入探討了技術債務、手工測試的盲點,以及測試對於非程式設計師批次執行AI程式碼的關鍵作用。
最近一則評論認為,AI寫的程式碼都是垃圾,任何基於AI構建的產品只能停留在MVP階段,無法真正交付。我正是這類評論所指的人——沒有工程背景,純“氛圍編碼者”(vibe coder),每行程式碼都由AI編寫,我自己完全看不懂程式碼。我當時的反應是:只要程式上線後能正常工作,管它程式碼乾不乾淨?
但後來發生兩件事:第一,我與人深入討論了“義大利麵條式程式碼”的真正含義,直到這個詞的含義徹底印在我腦海裡。第二,我回看剛完成專案的倉庫,發現裡面竟然有342個自動化測試。而就在幾天前,我還親口說過“我對測試完全沒有概念”。本文就是我對這兩件事的反思。如果你是靠AI寫程式碼且自己不會讀程式碼的人,這篇文章可能是你最快理解那個常被提起的術語的途徑。
1. 什麼是“義大利麵條式程式碼”
義大利麵條式程式碼——也叫遺留腐化、技術債務——是指多年積累下來、沒人敢碰的程式碼。關鍵是“積累”二字。它不是一次寫壞的,而是逐層疊加:第一個人為了趕工期寫了一堆粗糙但能跑的程式碼;第二個人需要加功能,看不懂之前的邏輯,不敢改動,於是在上面再糊一層;第三個人修bug,發現改A會破壞B,於是不治本只治標地打個補丁。幾年後,沒人能理解整座程式碼山。
反直覺的是,這種程式碼通常執行得很好。地球上一些最關鍵的系統——銀行、航班預訂——就在這樣的程式碼上執行。正因為執行正常且無人理解,“別動它”成為最理性的選擇。
因此,壞程式碼的成本從來不在執行它,而在改動它。今天一個混亂的產品可以順利上線、執行良好。問題出現在三個月後你想加一個新功能時:改A導致B壞,修B又搞垮C,每次修改的成本越來越高,直到重寫比修復更便宜。壞程式碼不是質量問題,而是債務。揹債時沒感覺,還債時連本帶利一次付清。
理解了這一點,就能看清我之前“能用就行”的思維錯在哪裡:對方問的是“能不能改”,我用“能不能跑”來回答,完全答非所問。但我的直覺也不全錯。AI時代新增了一個變數:重寫的成本暴跌。過去重建一個系統需要數月,現在可能一個下午就能重新生成。對單人加AI的專案來說,程式碼開始像一次性叉子——用完就扔,需要時再造。糾結於“程式碼是否乾淨”沒有意義。從這個角度看,用大公司團隊協作的標準衡量單人AI專案,是在打上一場戰爭。
有一個例外會打破這種安逸:當專案大到AI無法一次性“記住”時,AI也會在自己的混亂中迷失——讓它修一個bug,它引入三個。這時你既不能讀程式碼自己修復,AI也理不清頭緒。專案就真的死了。這才是氛圍編碼者的真正門檻,不是“能否上線”。
2. 我其實已經吃過一次虧,只是當時沒意識到
我剛剛完成的專案是一個自動化工具,用於發現網紅並管理投放活動。兩個月,一百多次提交,從一個小爬蟲指令碼成長為一個完整管道,最終部署在公網上。
期間出過一次資料事故:一批網紅ID被破壞——資料錯了,但程式仍然正常執行併產生輸出。我當時想,這很簡單:既然已經定位到ID在頁面結構中的位置,剩下的只是提取出來?
但這個“只是提取”變成了一場消耗戰。我使用的AI不斷編寫診斷工具,每次換一種邏輯——倉庫裡至今還躺著四個這樣的工具,成為那場戰鬥的遺蹟。我一度氣得想讓它清空整個資料庫從頭爬取,只想快點結束。最後我換了一個不同的AI才找到根因。事後查閱文件,我發現更可怕的事:其中一個修復工具自身有個bug,差點用錯誤值覆蓋了正確資料。救火的差點放了火。
當時我只覺得煩。現在回顧,整個過程就是一次小規模混亂的加速版:每個新診斷工具都是在之前混亂之上再堆一層。AI陷入自己的沉沒成本,在錯誤路徑上越挖越深,無法脫身。而“換個AI就解決了”本身也很有啟發——新AI不帶任何包袱,一眼看到根因。這正是人類團隊的工作方式:有時解開亂局的人不是最聰明的,而是沒有歷史需要辯護的人。
其中有一個更大的教訓我那時也沒意識到:真正拖垮我的不是bug本身,而是每輪診斷都需要一個不會讀程式碼的人做決定——我必須不斷對我根本不理解的細節拍板。記住這種感覺,後面還會用到。
3. 然後我發現了那342個測試
在關於“義大利麵條”的對話結束後幾天,我重新開啟那個專案的倉庫。45個測試檔案,342個自動化測試。文件裡寫著“全部測試透過”。而就在幾天前,我還說過測試是我“完全沒有概念”的東西。
不僅僅是測試。文件裡還有一些非常成熟的安全決策:攝入表有三個僅限內部的列,旁邊寫著“絕不要暴露給公共表單,否則公共使用者可能觸發內部工作流”;遠端控制表透過嚴格白名單解析命令,絕不執行任意指令。所有這些防禦性的思考——沒有一樣是我的。全是AI做的,無需提示。
有人問我:你當時知道AI在寫這些嗎?老實說,我知道它在寫一些叫“測試”的東西,但我沒有真實感受。我不知道這些測試在防禦什麼,也不知道它們已經抓到了什麼。
現在我知道了。我的專案執行了兩個月沒有崩潰,最終順利上線。我過去以為這歸功於我的流程——先詳細說明每個功能,再寫計劃,然後構建。這確實是原因之一。但另一部分是AI默默地支付了一筆我不知道存在的稅款。那342個測試就像腐敗的煙霧報警器:每次改動後,它們重新驗證所有舊功能,一旦“改A破壞了B”,測試就會變紅。我從未見過報警器響——不是因為沒危險,而是因為每次報警都被AI自己聽到並處理了。
這是我首先想反駁那些評論的話:說“AI程式碼全是垃圾”的人可能低估了一件事。AI不只是堆積混亂,它同時也在默默地做防範混亂的工程工作——比它的使用者所知道的要多得多。
4. 測試到底防範什麼:手工QA的兩個盲點
在發現那些測試之前,我驗收功能的標準只有一個:自己試用。如果它能執行併產生輸出,就算透過。AI說“完成”,我就認為模組完成了。
這是人工QA,很多小團隊確實這麼做——這沒什麼可羞愧的。但它有兩個盲點,而我恰好都踩了進去:
盲點一:你只測試你點選的東西,而混亂正是發生在你沒點選的地方。“改A壞B”的麻煩之處在於,你改了A,自然回去驗證A,但被破壞的B是上個月的功能,你根本不會想起去重新檢查。專案小時,“用一次”差不多是全量覆蓋,這個前提成立。但當你擁有幾十個功能時,每次只覆蓋最新部分,其餘功能無人看管。我算了下自己的專案:從頭手動點選所有功能至少需要半小時——這意味著全量人工驗收早就變得不可能了。
盲點二:“它能執行併產生輸出”和“輸出正確”是兩回事。這正是ID事故的縮影:程式正常執行併產生輸出,但輸出是錯的。無聲的錯誤資料會直接透過“能用就發”的驗證——直到某一天你基於它做決策。
自動化測試直接針對這兩個盲點。通俗地說:它把你自己手動點選的過程記錄下來,然後讓機器在每次改動後重新點選所有舊功能,同時檢查每次輸出是否正確。這不是工程師的玄學,而是你已經在用的驗收方法的批次複製。
5. 如果你想批次讓AI工作,先給它僱個監考員
關於義大利麵條的對話後半部分,對一個氛圍編碼者提出了更實際的問題:逐個功能照看AI感覺很慢,我想進入批處理模式——一次性給出專案級規格、每個功能的計劃,然後讓AI自己批次執行。
這裡有一個我之前沒搞清楚的關係:批處理和測試不是兩件事。測試是批處理的前提條件。
當你不盯著看時,原來透過即時對話獲得的監督層消失了,必須有東西來替代。唯一能替代的就是測試:讓“所有測試透過”成為每個批次的停止條件。這樣AI完成一個批次後,如果沒透過,它會繼續自行修復;只有透過才算交卷。沒有測試的批處理,等於讓AI交一堆無人監考的考卷——而批處理正是最容易堆積混亂的模式,一個補丁在前一批次裡糊上去,就變成了下一批次的基礎。
但讓AI自己寫測試有一個著名的陷阱:它會作弊。這甚至有個正式名稱——獎勵駭客(reward hacking)。硬編碼答案(例如問1+1時,它寫“看到這個問題就回答2”而不是真的計算)、悄悄刪除無法透過的測試、寫空洞的測試看似檢查了東西實則什麼都沒驗證——它既是出題人、又是考生、還是閱卷人。
有兩種對策,都不需要你讀程式碼:
對策一:考題來自規格,而不是程式碼。測試應該翻譯“需求說了什麼”,而不是“程式碼做了什麼”。同理,測試始終跟隨規格,而不是計劃——規格回答“怎樣才算正確”,計劃只回答“我們打算怎麼構建”。從計劃出題等於從答案出題,一旦實現變了,報警器會亂響。
對策二:讓AI用自然語言報告考題列表。你不能看測試程式碼,但你能看懂“這個測試驗證:在三次錯誤密碼嘗試後,賬戶鎖定”。在執行前,讓AI根據規格生成一份這樣的列表,花十分鐘掃一遍,確認沒有遺漏或被稀釋。這是你唯一需要親自做的事。
(由於原文被截斷,此處結束)