iT邦幫忙

2026 iThome 鐵人賽

DAY 4
2
Software Development

諸神也搖頭的 Legacy Code: 30天 .NET 工程師生存之道系列 第 4

Day 04 -「特洛伊木馬」單元測試?

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20260804/20182564FHbG6MyxwA.png

特洛伊戰爭打了十年,希臘聯軍始終攻不下特洛伊城,繼續硬打不是辦法,於是奧德修斯想出了一個計畫,打造一匹巨大的木馬,讓一批士兵躲進中空的馬腹裡,其餘軍隊則假裝撤退,把船開到看不見的地方躲起來。

隔天一早,特洛伊人打開城門,只看到空蕩蕩的海灘和一匹木馬,有人認為那是希臘人獻給神明的祭品,只要把它搬進城裡,就能得到神明的保佑。

祭司拉奧孔卻覺得事情不對,他拿起長矛刺向馬腹,裡面傳出低沉的聲響...他警告眾人,不要輕易相信希臘人留下的禮物,沒想到這時兩條巨蛇從海上出現,纏住了他和兩個兒子!!!

特洛伊人把這件事當成神明降下的懲罰,於是再也沒有人敢反對,最後還是把木馬拉進了城裡。

到了晚上,城裡的人大肆慶祝,以為長達十年的戰爭終於結束,等四周安靜下來,藏在木馬裡的士兵悄悄爬出來打開城門,讓原本躲在外面的希臘軍隊攻進城內,特洛伊城就此淪陷...

曾幾何時我們對「單元測試」的認知其實就是:

把每個 method 都測一次。
替每一行程式碼補上一個測試。

聽起來很合理,但只做到這些,就能叫作單元測試嗎?今天我們就來聊聊,單元測試真正重要的是什麼。


選邊站

修改程式碼,大致可以分成兩種做法。

Edit and Pray(改完再祈禱)

也是最常見的做法。仔細讀 code、想清楚要改哪裡、動手修改,再把系統跑起來看看有沒有壞,最後默默祈禱自己沒有踩到地雷。

這個過程聽起來其實很專業,畢竟我們有認真看、有仔細想,也有打開畫面測過,但問題是,再怎麼仔細都不能保證不出錯。

Michael Feathers 在《Working Effectively with Legacy Code》說過:

我們不會只因為一位外科醫生「動作很小心」,就放心讓他拿奶油刀替我們開刀。

PS: 超會比喻的...什麼時候才能寫文章到這種程度啊!

Cover and Modify(先覆蓋,再修改)

在動手之前,先用測試把目前的行為保護起來,修改之後再由測試告訴我們原本的行為有沒有被破壞。

這兩種派別的差別不在於「有沒有用心、有沒有花時間」,而在於我們有沒有把感覺換成回饋,如果每次改動都有一個人或工具能快速地給我們回饋,我們就能少走很多冤枉路,可惜不可能有人每天都在旁邊看你寫 Code,寫錯就打你一下,所以提供這個快速回饋的工具,就是單元測試。


把程式碼夾住

Feathers 也把測試形容成 Software Vise,也就是「軟體虎鉗」。

https://ithelp.ithome.com.tw/upload/images/20260804/20182564fIHVSs35nY.jpg
圖片擷取自網路

虎鉗是木工或金工常用的工具,它會用兩塊夾片固定住工件,讓他在加工時不會亂跑。

當一段 code 被測試覆蓋,它的行為就被「夾」在那裡,動的時候只有需要加工的那一小塊會變動,其他部分不會無聲無息地溜走,除非我們夾得不夠緊(測試沒有保護到的地方)。

這也是為什麼接手 Legacy Code 時,我們特別需要單元測試。不是因為寫了測試就能保證永遠不出錯,而是我們需要一套夠快、可以頻繁執行的回饋機制,否則一個看似不起眼的誤差,都可能讓其他零件跟著錯位,最後影響整個結構的運作。


黃金三角

提到這,就不能不一起談談大名鼎鼎的測試金字塔。

https://ithelp.ithome.com.tw/upload/images/20260804/20182564L7s3wkTeXC.png

層級 主要目的 速度 執行時機
單元 驗證「商業邏輯或行為」 毫秒級 每改一行都該能跑
整合 驗證「邏輯跟外部系統能否正確合作」 秒~分鐘級 每天跑、CI、合併前或部署前跑
E2E 驗證「使用者能否從頭到尾完成完整流程」 分鐘級以上 部署至測試環境後、正式上線前跑

測試金字塔提供了一個安排自動化測試的方向:

越靠近程式碼底層的測試,數量通常越多
越接近完整使用情境的測試,數量通常越少

原因不難理解,底層測試通常跑得快,失敗時也比較容易找到問題,越高層的測試雖然更接近真實使用情境,執行和維護的成本通常也越高,而且更容易受到資料、網路與環境設定影響。

不是說 整合測試E2E測試 沒用,它們測的是真正的組合行為,這個層面的測試很有價值,問題是它們的成本比較高,跑得慢、環境要配好、資料要準備,一個環境問題也許就能讓它紅燈,而紅燈跟業務邏輯可能完全無關,不過其實在現代都有工具可以幫我們解決這些問題,在後面的章節我們會提到。


講那麼多,到底什麼是「單元測試」?

那麼,到底什麼才算單元測試?

「單元」的意義其實有不同流派的看法,不一定只能是一個 method 或一個 class,與其說明什麼是單元測試,不如我們先來看看幾個反定義:

  1. 需要透過網路通訊
  2. 會碰到檔案系統
  3. 執行前必須特別調整環境,例如修改設定檔
  4. 會連到資料庫

聽起來很苛刻吧?我第一次看到的時候也想說:「不會太極端嗎?我這個測試明明就是在驗證 BLL 的行為啊,只是順便連了一下 DB……」

關鍵不在於它驗證的是什麼,而在於我們希望單元測試具備兩個特性:

  1. 跑得快
  2. 失敗時容易定位問題

我們需要的是快速而明確的回饋,當測試連到資料庫,它通常會跑得比較慢,而紅燈出現時,也得繼續確認到底是 DB 連線、測試資料,還是業務邏輯出了問題。

捫心自問一下,我們真的會在每次小幅修改後,都願意跑一次這麼慢的測試嗎?

不會的,我們會「再改個幾次再跑」、「下班前跑」、「明天再說」,然後測試就慢慢變成擺設,再也沒人在意它紅了還是綠了。

試想一下,如果每次改完 Code,準備要來跑測試了,但是每次跑都要 5 分鐘左右,你會不會很煩躁?好吧,也許可以趁這段時間去泡杯咖啡,回來剛好跑完,然後我們就會忘記剛剛到底做了什麼,也就是強制被斷心流了!好吧,也許我們的 Context 超大,很肯定自己不會被斷心流,但當興致沖沖的又修改完一段邏輯的時候,不就還要再跑一次嗎,難道還要再去泡一杯咖啡嗎?對吧!

我們都不喜歡乾等待,會讓我們的耐心漸漸消逝,直到最後這些測試就會被我們收起來,再也不會拿出來,跟每次驗收、部署前都會跑的整合測試不同,這是兩件截然不同的事情,最好的情況下,我們希望當下立刻就能知道狀況,這才是單元測試,因為當我們養成工作習慣是改個兩三步驟就跑一下測試,小步快跑如其名就是要快,更準確的說是回饋要快,那麼在面對 Legacy Code 時很多錯誤都會在這一步就被我們攔截並且定位住。


舉個例子

假設需求是「會員累積消費滿一萬元,就升等為金卡會員。」

[Fact]
public void GetMemberTier_當客戶累積消費滿一萬元_應回傳金卡()
{
    // 連到開發環境的資料庫
    var conn = new SqlConnection("Server=dev-db;Database=HelloShop;...");
    var loyalty = new LoyaltyService(conn);

    // 執行測試前,先新增會員與消費紀錄
    InsertCustomerForTest(id: 1);
    InsertPurchaseHistory(customerId: 1, totalAmount: 12000);

    var profile = loyalty.GetMemberTier(customerId: 1);

    Assert.Equal("Gold", profile.Tier);
}

這段測試確實有價值,因為它能驗證 LoyaltyService 和資料庫接起來後的行為。但是依照前面我們對於「單元測試」的標準,它同時具備三個特徵:

  • 會連到資料庫
  • 需要透過網路連到 dev-db
  • 執行前要先準備會員與消費紀錄

因此,更準確地說,這是一個整合測試,而不是單元測試。


那高層級測試還要不要寫?

要,只是要分清楚它們各自在解決什麼問題。

設計良好的高層級測試,確實能涵蓋許多單元測試也會驗證到的行為,而且更接近系統真正運作的方式,但這不代表它能完全取代單元測試,兩者不是互相競爭,而是分工合作。

所以我會採取層層堆疊的方式來構築測試,其實什麼測試都需要,討論的場景不同而已。

互補但不互換,無法在下層驗證的重要行為,就需要往上層測


總結

每個專案的測試策略都不同,會依照每個地方的重要程度,考量出錯了會對客戶帶來多大的影響程度,而選用適當的測試策略及數量,因為如果都不用權衡的話那就全部都 E2E 就好了啊,最貼近使用者了您說是吧?那我們都知道後果將會是大災難對吧 XD

https://ithelp.ithome.com.tw/upload/images/20260804/20182564qxnu3PtDz2.png
圖片擷取自網路

再次強調,我不是什麼反金字塔頂端的奇怪份子,我們不是要排斥測試金字塔上層的測試,該驗證的還是要驗證,只是當我們接手複雜的 Legacy Code,需要頻繁修改、快速確認結果時,單元測試通常是極其重要的工具。

所以本系列文章大多都會以單元測試做為範例,後續章節才會談到些許整合測試的部分。

明天我們就要進入實戰篇了,就先來看看:為什麼感覺不到程式碼?

Reference


上一篇
Day 03 -「點石成金」改變軟體的四種理由
下一篇
Day 05 -「阿基里斯之踵」如何感覺到程式碼?
系列文
諸神也搖頭的 Legacy Code: 30天 .NET 工程師生存之道5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言