程式碼測試意味著完全消滅了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 語言為例,分析其原因。
作者 | 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\nwant: %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
(本文為AI科技大本營轉載文章,轉載請聯絡作者。)
徵稿推薦閱讀:
Windows 95被做成了一款軟體,可玩掃雷和紙牌
給Chrome“捉蟲”16000個,Google開源bug自檢工具
2019全球AI 100強,中國佔獨角獸半壁江山,但憂患暗存
“百練”成鋼:NumPy 100練
這4門AI網課極具人氣,逆天好評!(附程式碼+答疑)
點選“閱讀原文”,開啟CSDN APP 閱讀更貼心!
相關文章
- C# 測試程式碼#if DEBUG使用C#
- 程式碼Bug太多?給新人Code Review頭都大了?快來試試SpotBugsView
- 消滅 Bug!推薦5款測試員不可不知的bug管理工具!
- 消滅Bug!十款免費移動應用測試框架推薦框架
- 測試程式碼
- MYSQL程式碼顯示測試測試MySql
- 測試程式碼高亮
- Android惡意程式碼分析與滲透測試 程式碼排版太差了Android
- 程式碼寫作測試
- 代理類測試程式碼
- phpunit測試成功phpunit測試實踐程式碼PHP
- 先讓不懂程式碼的來測?通義這個新產品,程式碼剛寫完,預覽就出來了
- 消滅 Java 程式碼的“壞味道”Java
- 要炸了!剛寫完這段程式碼,就被開除了
- 讀完《程式碼大全》
- Debug模式下,測試app字尾名“-測試”模式APP
- 白盒測試程式碼應該怎麼測試
- 測試你的前端程式碼:視覺化測試前端視覺化
- 伺服器上的程式碼怎麼可以在本地測試執行Debug伺服器
- Web測試中定位bug方法Web
- 程式碼測試用例指南
- 寫 Laravel 測試程式碼 (五)Laravel
- 寫 Laravel 測試程式碼 (二)Laravel
- 同事改Bug飛快,原來掌握了這些程式碼Debug技巧
- 程式設計師修煉第一課 | 如何通過改善程式碼風格來消滅隱藏bug程式設計師
- 測試你的前端程式碼 – part4(整合測試)前端
- 安卓 unit 測試與 instrument 測試的程式碼共享安卓
- Web服務效能測試:Node完勝JavaWebJava
- 寫給程式設計師的軟體測試指南:人人都可以開發無Bug程式碼程式設計師
- 今天寫了一個可以測試併發數和執行次數的壓力測試程式碼。(Java)Java
- 軟體測試中bug淺析
- 使用 xunit 編寫測試程式碼
- go 程式碼覆蓋率測試Go
- 程式碼測試覆蓋率分析
- python語法-測試程式碼Python
- mysql鎖 實戰測試程式碼MySql
- 使用 intern 編寫測試程式碼
- 無聊程式碼,測試bootstrap.boot