🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

開篇:開發者的傲慢

那天是星期五晚上,11點47分。我的測試套件?一切順利。覆蓋率?高達94.7%。我的程式碼?太棒了! !我為自己感到驕傲…

我做的一切都是對的:

  • 單元測試 - 已檢查

  • 整合測試 - 已檢查

  • 已涵蓋特殊情況 - 已確認

  • 上個迭代周期裡那個奇怪的bug?已修復並測試—已驗證。

  • 我的自信?爆棚——已驗證

我把工作推到生產環境,合上筆記型電腦,發出令人滿意的「啪嗒」一聲,然後收拾行囊。這個週末,我要征服卡納塔克邦的凱瓦拉貝塔山,這是一條海拔約1280公尺的健行路線。因為我習慣把筆記型電腦帶上山(別笑話我,我們每個人都有自己的小毛病),所以我想,萬一出了什麼事,至少在山頂還有手機訊號。


墜落:當現實(字面上的)襲來

週六凌晨4點。我站在凱瓦拉貝塔山腳下,呼吸著清爽的山間空氣,感覺自己像是成長題材開發者紀錄片的主角。這時,我的手機震動起來。

Teams 通知:“生產環境已關閉。緊急!”

但是,嘿,我還有筆記型電腦!我這就……看看手機訊號……我爬快點,這樣訊號就更好了!

等我終於找到4G訊號還不錯的地方(大概在3000英尺高的地方,汗流浹背,就像黑色星期五的部署流水線一樣),我趴在一塊石頭上打開了筆記型電腦。想像一下:開發人員們從我身邊走過,享受著他們最愜意的無電子設備生活,而我卻像咕嚕抱著他的寶貝一樣,蹲在我的MacBook前。

這些原木…很有創意

NoneType object has no attribute 'execute'
KeyError: 'user_preferences'
Connection timeout: Database not responding

困惑的尼克楊表情包

我那94.7%的程式碼覆蓋率竟然沒涵蓋到這一點。我精心寫的單元測試竟然沒發現那一點。至於我周五晚上那點自信?現在就像印第安納瓊斯電影裡的巨石一樣,從山上滾落而去。

印第安納瓊斯中的巨石

令人不安的事實:單元測試是必要的,但是…

接下來我要說的觀點可能會引起爭議,也會在推特上被噴:單元測試固然必要,但並非萬能靈藥。它們更像是只保護軀幹的防彈背心。

在你厭惡地關閉這個頁面之前,請容許我解釋一下。

究竟出了什麼問題? (從4200英尺高空進行的事故分析)

我的測試完美無缺,我的程式碼也完美無缺。問題出在哪裡呢?

  1. 環境差異:我的生產環境使用了不同的 Python 版本(3.9 對比 3.11),導致字典合併的處理方式不同。

  2. 競態條件:我的非同步資料庫連線存在競態條件,僅在負載較高時出現。

  3. 配置地獄:生產環境中的環境變數設定不同。

這些問題都沒有被單元測試發現。因為單元測試測試的是單元,是隔離的、模擬的、完美的小世界,在這個小世界裡,所有東西都完全按照你的指示執行。

真正的生產?那簡直就是一片無法無天的荒地,墨菲定律會騎著摩托車衝進你精心建造的沙堡。


所以……單元測試真的沒用嗎?

不,絕對不行。放下武器!

關鍵在於:單元測試就像去健身房。就算你還會絆倒摔下樓梯,也不代表練腿日毫無意義。

單元測試捕獲到:

  • 函數中的邏輯錯誤

  • 凌晨兩點你忘記的那些極端狀況

  • 重構時出現回歸

  • 那次你不小心刪掉了一個關鍵的if語句

  • 你同事的「快速修復」方法卻又弄壞了其他三樣東西。

他們沒發現的:

  • 現實世界的混亂

  • 系統整合問題

  • 基礎設施問題

  • 那台被惡魔附身的伺服器

  • 宇宙不可避免的熱寂


如何編寫真正有意義的測試(來自岩石的啟示)

坐在山頂上,除錯著生產程式碼,手機電量只剩12%的時候,我突然頓悟了。正確的測試方法如下:

1. 測試金字塔是你的好幫手

測試階段

不要只待在地下室進行單位測試,偶爾也要上樓看看。

2. 測試合同,而非實現

壞的:

def test_user_service_calls_database_exactly_once():
    # This test knows too much about HOW things work
    mock_db.assert_called_once()

好的:

def test_user_service_returns_user_data():
    # This test cares about WHAT happens
    user = service.get_user(123)
    assert user.id == 123
    assert user.email is not None

3. 整合測試並非可有可無

def test_the_actual_freaking_database():
    """Yes, spin up a test database. Yes, it's slower. 
       Yes, it would've caught my production bug."""
    db = create_test_database()
    result = service.fetch_user_preferences(user_id=1)
    assert result is not None  # THIS WOULD'VE FAILED

4. 檢驗你的假設

我的程式碼假設資料庫連線始終存在。哈哈。

def test_graceful_degradation_when_everything_is_on_fire():
    with mock.patch('db.connect', side_effect=TimeoutError):
        # Does your app explode or handle it gracefully?
        result = service.get_user(123)
        assert result == None or isinstance(result, DefaultUser)

5. 對外部 API 使用契約測試

def test_third_party_api_still_returns_what_we_expect():
    """Because apparently they can just CHANGE THINGS"""
    response = external_api.get_data()
    assert 'user_id' in response  # Would've caught the API change
    assert isinstance(response['user_id'], int)

山的智慧:一種平衡的方法

當我終於修復了這個漏洞(當時海拔3800英尺,電池電量僅剩4%)時,我意識到了一件事:

考試就像徒步旅行。你需要做好準備,但也要做好應對意外狀況的準備。

您的測試策略應包括:

  1. 單元測試(60%) :快速、專注、無所畏懼

  2. 整合測試(30%) :速度較慢、範圍更廣、更貼近實際。

  3. 端對端測試(10%) :耗時最長、範圍最廣、最痛苦,但也最有價值

  4. 手動測試:有時候你只需要四處點擊,看看哪裡出了問題。

  5. 監控:因為測試無法發現所有問題,但儀錶板可以在出現嚴重問題時發出警報。


真正具有爭議性的觀點

這就是我接下來要說的,最勁爆的觀點,一定會讓我被開發者圈抵制:

別再追求100%的程式碼覆蓋率了,開始追求程式碼覆蓋率的置信度吧。

你更想要:

  • 100% 單元測試覆蓋率,但沒有整合測試?

  • 70% 的覆蓋率,且單元測試、整合測試和端對端測試的混合使用是否合理?

我選擇方案 B,這樣晚上就能睡得更好(如果我不在山上除錯設備的話)。


真正有效的考試實用技巧

1. 寫出能說故事的測試

def test_user_cannot_access_deleted_account():
    """Given: A user with a deleted account
       When: They try to log in
       Then: They should get a meaningful error"""
    # Your test here

2. 測試悲傷之路

每個人都會試著走上幸福之路。要與眾不同。要悲傷。

def test_what_happens_when_literally_everything_fails():
    # Database down? Check.
    # Cache unavailable? Check.  
    # API returning 500s? Check.
    # Does your app at least log the error? Let's find out!

3. 使用真實的測試資料

# Bad
test_email = "[email protected]"

# Good  
test_email = "[email protected]"  # Tests plus signs, subdomains, etc.

4. CI/CD 不是可選項

如果你的測試只在你的機器上執行,那它們基本上只是感覺而已。

5. 快速失敗,更快學習

不要花三個小時除錯一個測試。如果測試這麼複雜,也許你的程式碼太複雜了。


尾聲:與不完美和解

我最後在山上修復了那個bug(多虧了那不穩定的4G網路)。徒步旅行很美,即使我有一半的時間都在除錯。而且我還學到了重要的一課:

俯視圖

完美是優秀的敵人,但好的測驗總比你從未寫的完美測試好。

單元測試是必要的,但還不夠。它們只是工具箱中的一種工具,而不是全部。它們可以幫你發現拼字錯誤、邏輯錯誤,以及那些讓你懊惱不已的「天哪,我當時到底在想什麼」的時刻。但它們無法發現所有問題。

這樣也沒關係。

歸根究底,發展就是:

  • 編寫(大部分情況下)能執行的程式碼

  • 測試你能測試的內容

  • 監控那些無法測試的事物

  • 在海拔 4200 英尺的山頂上,電池電量僅剩 2%,隨時準備進行緊急修復。

  • 從每一次生產火災中吸取教訓


TL;DR(因為現在沒人看長文了)

編寫單元測試。編寫整合測試。編寫端對端測試。不要過度在意程式碼覆蓋率。測試所有可能出錯的路徑。監控生產環境。也許不要在周五晚上部署,尤其是在徒步旅行之前。一定要帶上行動電源。

單元測試就像安全氣囊。絕對必要,但你仍然需要小心駕駛。

現在請允許我告退,我要和我的 DevOps 團隊開會,討論「為什麼在周五晚上部署是反人類罪行」。


你也有過類似的考試恐怖經驗嗎?歡迎在評論區分享。我不想獨自承受這些痛苦。

我登頂了。景色很美,不虛此行。至於製作過程中的崩壞呢?那就不值了。

我處於巔峰


原文出處:https://dev.to/varshithvhegde/unit-tests-the-greatest-lie-we-tell-ourselves-2ehd


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝13   💬4   ❤️4
434
🥈
我愛JS
📝1   💬3   ❤️2
46
🥉
酷豪
1
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付