當前位置:
首頁 > 知識 > 代碼測試意味著完全消滅了 Bug?

代碼測試意味著完全消滅了 Bug?

日前,一位名為 Jens Neuse 的開發者在改進其 graphql 解析庫的過程中,發現詞法分析器和解析器中存在很多的低效率,因此不得不重構完整的代碼庫(https://medium.com/@jens.neuse/want-to-write-good-unit-tests-in-go-dont-panic-or-should-you-ba3eb5bf4f51)。在重構的過程中,Jens Neuse 認為測試至關重要。然而,本文作者卻並不這麼想,他認為測試並不意味著一切,接下本文將以 Go 語言為例,分析其原因。

代碼測試意味著完全消滅了 Bug?


作者 | martin

譯者 | 梁蕊

責編 | 屠敏

出品 | CSDN(ID:CSDNNews)

我使用過的一些最難用的代碼是「易於測試」的代碼。代碼將所有內容抽象到開發者難以想像發生了什麼的程度,只是為了向原本非常簡單的函數中添加「單元測試」。DHH 稱這種為測試引起的設計損壞。


測試只是確保用戶的程序正常運行的工具之一。另外一種非常重要的工具是以一種易於理解和推理(簡單)的方式編寫代碼。

在此,推薦開發者可以查閱一本使用廣泛的測試書籍,Robert C.Martin 編寫的《Clean Code》,其中部分內容是為了響應更複雜的代碼而寫的,在這些程序中,你閱讀了 1000 行代碼,但仍然不知道發生了什麼。我最近不得不將一個簡單的 Java 「表情符號替代品」(:joy:→)移植到 Go。為了確保兼容性,我查看了它的實現類。這包含了一大堆類、工廠、以及所有這些只會導致在字元串上調用 regexp 的東西。

在像 Ruby 和 Python 這樣的動態語言中,測試對於不同的前提很重要,就像下面這段代碼將會正常工作:

if condition:
print("w00t")
else:
nonexistent_function()

當然,除了如果進入 else 分支,很容易會拼寫錯誤的東西或者混合東西。

在 Go 語言中,這些問題都不那麼令人擔憂。Go 有一個靜態類型系統,重點是可以編寫簡單直接的代碼,易於理解。即使對於許多動態語言,也有可選的輸入系統(Python 中的函數注釋,JavaScript 的 TypeSript)。

有時你可以做一個簡單的實現,而不犧牲任何可測試性;太棒了!但是有時你必須找到一個平衡點。對於某些代碼,不添加單元測試是可以的。

對「單元測試」的過分關注可能會對代碼庫造成難以置信的損害。有些代碼庫有大量的單元測試,這使得任何更改都非常耗時,因為你要為哪怕是很小的更改而修復一大堆測試。很多時候,這些測試都是重複的;像簡單的 CRUD,HTTP 端點的每一層添加一個測試是一個常見的示例。在許多應用程序中,只依賴一個集成測試就可以了。

像 SQL 模擬這樣的東西是另一個很好的例子。它使代碼更複雜,更難更改,所以可以說我們添加了一個「單元測試」 select * from foo where x = ?。最糟糕的是,除了驗證你沒有錯誤的查詢 SQL 查詢之外,它甚至不測試任何其他內容。一旦測試開始做任何有用的事情,例如驗證它實際上從資料庫中返回正確的行,單元測試純粹主義者開始抱怨它並不是真正的單元測試,你做錯了。

對於大多數查詢,集成測試和/或手動測試都是很好的,並且廣泛的 SQL 模擬充其量是多餘的,並且在最壞的情況下是有害的。

當然也有例外:如果你有很多的 if cond {q += 「more sql」} 那麼添加 SQL 模擬來驗證邏輯的正確性可能是一個好主意。即使在那些情況下,」非單元的單元測試(例如,僅訪問資料庫的那個)仍然是可行的選擇。集成測試也是一種選擇。很多應用程序無論如何都沒有那種複雜的查詢。

關注單元測試的一個重要原因是確保測試代碼能夠快速運行。這是對需要一天運行的大規模測試工具的響應。這在 Go 中也不是一個真正的問題。我編寫的所有集成測試都在合理的時間內運行(最多幾秒,通常更快)。GO 1.10 中引入的測試緩存使它不再受關注。

去年,一位同事重構了我們基於 ETag 的緩存庫。舊代碼非常直接且易於理解,雖然我沒有聲稱它一定沒有 Bug,但它確實在很長一段時間內都運行良好。

它應該已經在適當的地方寫了一些測試,但它沒有(我沒有寫原始版本)。請注意,代碼並非完全沒有經過測試,因為我們確實進行了集成測試。

重構的版本要複雜得多。除了花了兩周時間將一段工作代碼重構成另一段工作代碼(另一篇文章的主題)之外,我並不相信它實際上要好得多。我認為自己是一位有一定造詣且經驗豐富的程序員,在 Go 中擁有合理的知識和經驗。總的來說,根據同行和績效評估的反饋,我至少是「平均」技能水平的程序員,如果不是更多的話。

如果一個普通的程序員因為有很多層的抽象而難以理解一些簡單的函數的本質,那麼一定是出現了問題。重構提供了一個工具用另一個測試用例來驗證正確性(簡單性)。簡單性很難保證正確性,但單元測試也不是。理想情況下,我們應該兩點都做到。


後記:重構引入了一個 Bug 並刪除了一個有用的功能,但現在更難添加,至少因為代碼要複雜得多。

所有單元正常工作都不能保證程序正常工作。很多邏輯錯誤都不會被捕獲,因為邏輯由幾個單元一起工作組成。所以你需要集成測試,如果集成測試重複了一半的單元測試,那麼為什麼還要為這些單元測試煩惱呢?

測試驅動開發(TDD)也只是一種工具。它可以很好的解決一些問題; 對其他人而言並非如此。特別是,我認為「被迫在小單元編寫代碼」 在某些情況下會非常有害。有些代碼只是一個串列腳本,上面寫著「執行此操作,然後執行此操作,然後執行此操作」。在一大堆「小單元」中拆分它可以大大減少代碼理解的容易程度,因此更難以驗證它是否正確。

我必須修復一些 Ruby 代碼,其中所有東西都是小單元。在 Ruby 社區中有一種強大的 TDD 文化,儘管單元很容易理解,但我發現理解應用程序邏輯非常困難。如果所有內容都以「小單位」分割,那麼理解所有內容如何組合在一起以創建一個有用的實際程序將會更加困難。

你可以看到舊微內核與單片內核爭論相同的摩擦,或者更近期的微服務與單片應用程序之間的摩擦。在原則上把所有東西分成一個個小的部分聽起來像一個偉大的想法,但在實踐中事實證明,使所有的小零件一起工作是一個非常困難的問題。混合方法似乎最適合內核和應用程序設計,平衡兩種方法的優點和缺點。我認為這同樣適用於代碼。

需要澄清的是,我並不是反對單元測試或 TDD,並且聲稱我們所有人都應該按照生活中的方式編寫代碼。我編寫單元測試並在有意義的時候實踐 TDD。我的觀點是,單元測試和 TDD 不是最後一個問題的解決方案,他們不應該不加區別的使用。這就是為什麼我頻繁的使用諸如「some」和「often」之類的單詞。

這讓我想到了測試框架的主題。我從來沒有理解像 goblin 這樣的庫正在解決什麼問題。這怎麼樣:

Expect(err).To(nil)
Expect(out).To(test.wantOut)

對此有所改進?

if err != nil {
t.Fatal(err)
}
if out != tt.want {
t.Errorf("out: %q
want: %q", out, tt.want)
}

if 和==怎麼了?為什麼我們需要抽象呢?請注意,對於表驅動的測試中,您只需鍵入一次這些檢查,因此您只需在此處保存幾行。

Ginkgo 更糟糕。它變成了一個非常簡單,直接且易於理解的代碼片段,並且不僅僅是抽象的if,它還可以在幾個不同的函數中完成執行(BeforeEach()和 DescribeTable())。

這稱為行為驅動開發(BDD)。我不完全確定如何看待 BDD。我持懷疑態度,但我從來沒有在一個大型項目中正確使用它,所以我猶豫不決是否放棄他。請注意,我說「正確」:大多數項目並不真正使用 BDD,他們只是使用帶有 BDD 語法的庫,並將其測試代碼插入其中。那是特別的 BDD,或者說是偽 BDD。

無論 BDD 有什麼優點,由於你的測試代碼類似於 BDD 風格的語法,所以這些優點都不會顯現出來。這本身就證明了 BDD 對許多項目來說可能不是一個好主意。

我認為這些 BDD(-ish)測試工具存在實際問題,因為它們混淆了你實際做的事情。無論如何,測試仍然是獲取函數的輸出並檢查它是否符合你的預期。沒有任何測試方法會改變這種基本原理。你添加的層越多,調試就越困難。

在確定某樣東西是否「容易」時,我最關心的不是編寫該東西是多麼容易,而是當事情失敗時調試是多麼容易。如果這樣可以讓事情變得更容易調試,那麼我很樂意花更多的精力寫一些東西。

所有代碼(包括測試代碼)都可能以令人困惑,令人驚訝和意外的方式(「錯誤」)失敗,然後你需要調試該代碼。代碼越複雜,調試起來就越困難。

程序員應該期望所有代碼(包括測試代碼)都要經歷幾個調試周期。請注意,對於調試周期,我並不是說「你需要修復的代碼中存在錯誤」,而是「我需要查看此代碼來修復錯誤」。

一般來說,我已經發現測試代碼比常規代碼更難調試,因為「代碼表面」往往更大。開發者需要考慮測試代碼和實際實現代碼。而不僅僅是考慮實現代碼。

添加這些抽象意味著你現在也必須考慮這一點!如果抽象會減少你必須考慮的範圍,這可能是可以的,這是在常規代碼中添加抽象的常見原因,但事實並非如此。它只是增加了更多需要考慮的東西。

所以這些都是錯誤的抽象:它們包裝和混淆,而不是分離關注點並縮小範圍。

如果你有興趣在開源項目中請求其他人來貢獻,那麼測試可以理解是一個非常重要的問題。

看到 PRs 上寫著「這是代碼,它可以工作,但我無法弄清楚測試,請暫停!」這並不罕見; 而且我很確定至少有幾個人甚至從不打算提交 PR 只是因為他們被困在測試中。我知道我有。

有一個開源項目是我貢獻的,我也想為之貢獻更多,但是我沒有,因為編寫和運行測試太難了。每一個變化都是「在 15 分鐘內編寫工作代碼,花 45 分鐘處理測試」。這一點兒也不好玩。

編寫好的軟體真的很難。當前我有一些關於如何實現好的軟體的想法,但沒有完整的實施方案。我知道「總是添加單元測試」和「總是使用 TDD」不是答案,儘管它們是有用的概念。打個比方:大多數人會同意自由市場是一個好主意,但與此同時,即使大多數自由主義者同意,但這並不是解決所有問題的完整方案。

原文:https://arp242.net/weblog/testing.html

本文為 CSDN 翻譯,如需轉載,請註明來源出處。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 CSDN 的精彩文章:

科學家之歿,竟是區塊鏈之過?
為什麼程序員對舊代碼深惡痛絕?

TAG:CSDN |