作者 | Jorge Fernández Rodriguez
譯者 | 明知山
策劃 | 丁曉昀
關鍵要點
當前的問題
軟體工程與其他職業相比具體它的特殊性,我想你會同意這樣的說法。技術的變化劇烈而迅速,僅僅是跟上時代發展的步伐就需要耗費大量的腦力。
也許正因為如此,我們會停留在一些公認的常規實踐或想法(即使它們會給我們帶來麻煩或不適用於某些場景)上。這些實踐試圖涵蓋大多數情況,但實際上又無法涵蓋所有情況。不管怎樣,這些實踐給了我們安慰。我們需要一些不會改變的東西讓我們感到安全,讓我們的大腦從思考變化的負擔中解脫出來,我們進入了自動駕駛模式。
這裡的問題在於,我們希望軟體開發像裝配線一樣:一旦裝配線建立起來,就永遠不再需要去碰它。我們開始變得一成不變。或許這在一段時間內適用於我們的 CI/CD 管道,但遺憾的是,它並不總是適用於我們的代碼。
糟糕的是,有時候信息經過多輪的輾轉遠離了它的本質。到了某個時候,我們甚至把這些實踐作為我們自身的一部分,我們捍衛它們,拒絕接受不同的觀點。大部分時候,我們只是想融入其中,不想提出新的想法。
在編寫代碼時,我們需要與之鬥爭,檢驗這些實踐是否適合當前的場景。假設我們將「最佳實踐」看作「最佳通用實踐」。
我們以敏捷可能被誤解的方式為例——在某些情況下,人們丟掉了敏捷的精髓。
在那篇文章中,我表達了人們經常會忘記敏捷的本質,因為很多時候敏捷的實現關注的是錯誤的東西。根據定義,敏捷的東西可以很容易改變方向並對變化快速做出響應。
我們嘗試用不同性質的實踐來實現這種響應性:技術上的,比如 CI/CD(持續集成 / 持續部署),戰略上的,比如疊代開發。然而,在處理軟體開發的核心部分——編碼時,我們經常會忘記了敏捷。想像一下你如何在沒有主要食材的情況下準備你最喜歡的餐點。我們在不考慮代碼的情況下追求敏捷性跟這個如出一轍。
這很可能會發生,因為改進代碼質量看起來很可怕,很複雜,或者很容易掉入兔子洞陷阱。也許只是因為我們不容易看到一些解決方案對我們的可操作性產生的負面影響,把未來的開發變成了一場噩夢:敏捷的反面。我們沒有把注意力集中在代碼上,而是放在了讓我們的流程(如 Scrum 等方法論)變得完美上,但這些流程可能並不太重要,我們還試圖在不解決主要本質問題的情況下去解決其他問題。
最後,我建議提升代碼對未來開發的影響(以及業務的未來)的可見性。或許 AI 能夠幫助我們量化這些東西,它不僅可以告訴我們開發質量,還可以根據我們的潛在選擇預測開發速度會有多慢。我認為這樣可以幫助公司意識到他們需要在可持續的開發上進行更多的投入。關於何時解決技術債務的討論將成為歷史。
在本文中,我將重點關注一種編碼實踐,它在敏捷開發中起著至關重要的作用,但很少受到質疑:傳統的測試方法。
我還將介紹「變化免疫力」,這是我最近發現的一種策略,它可以幫助我們實現目標和改變一些習慣,不僅是編程習慣,還有生活中的其他習慣。此外,我還將解釋我在上面的那篇文章中提到的一個觀點:我們希望通過改變一些實踐或習慣來實現可操作性,但卻在不知不覺中掉入陷阱,因為我們忽視了編碼在實現這一目標中所扮演的角色。
測試——是安全網還是束縛衣
我們都希望好的自動化測試會給我們帶來巨大的好處:我們可以修改代碼並快速驗證現有的功能不會出現中斷。測試成了我們的安全網。
然而,它們並不是免費的,它們是有成本的,而且會在一段時間後顯現出來,我們需要對其做出補償。測試運行的次數越多,我們從中獲得的價值就越大。但如果我們修改了測試,測試的「收益計數器」將重置為零。
除了補償成本之外,我們還需要考慮另一件事:在創建測試時,我們盡最大努力保證測試是正確的,但我們又不能百分之百確定。如果我們能確定我們所寫的代碼是正確的,那就不需要測試了,對吧?
測試只是給了我們信心,每次運行測試都會給我們增加一點信心。如果運行 1000 次,我們就獲得 1000 點信心。
如果在某個時間點測試中發現了一個 bug,我們就會失去這 1000 點信心。我們認為這是在保護我們,但事實並非如此。
類似的,如果我們對邏輯進行重構,並且測試發生了中斷,我們就不能確定是業務邏輯不對還是測試不對。我們丟掉了之前積累的「信心」,需要重新構建一個新的安全網。
這是對未來的投資。人們很少考慮長期投資,但值得慶幸的是,自動化測試的好處已經被廣泛接受(儘管我不時看到有人仍然不願意擁抱自動化測試)。
遺憾的是,其他長期投資,比如通過重構來保持代碼的靈活性,並沒有被廣泛接受。希望本文中提到的這些「完全非創新」的想法能夠幫助你消除這個障礙。
每次在添加新功能時,我都會感謝已經編寫好的測試,特別是在包含了眾多功能的大型服務中。我無法想像手動去驗證每一個功能點意味著什麼,那可能是個無稽之談。
然而,如果我們不遵循恰當的策略,它們也會對我們不利。不恰當的測試策略會減慢交付速度,並影響開發者體驗。
我們可以問自己下面這些問題。
你所有的單元測試都是單獨測試類或方法嗎?
如果答案是肯定的,並且你正在大量模擬依賴項,那麼你很可能會對一個接一個地模擬依賴項感到厭倦。在某些情況下,你會發現模擬不夠真實,並且代碼的邏輯實際上並沒有按照應有的方式執行。但願你能在發布之前發現這個問題,並且不需要作太多的修改。
你是否有時候覺得測試限制了你修改代碼的方式?
我猜你的答案也是肯定的。有時候,已有的代碼不再適合新的功能,或者變得過於複雜。你決定重構它。重構可能需要 10 或 15 分鐘,因為這是一個小的修改。然後,許多測試突然間無法編譯,或者執行失敗。調整和執行測試可能需要大量的時間,甚至比修改代碼要長得多。為了 10 到 15 分鐘的代碼修改,你最終會花上幾天的時間來調整測試。為什麼會這樣?代碼的行為並沒有發生變化啊!
如果功能沒有發生變化,那麼理想情況下測試也應該不會發生中斷。如果你調整了測試,能再次進入安全網嗎?請記住,如果你修改了測試,之前獲得的「收益計數器」和「信心計數器」將被重置。
稍後我們將看到,在許多情況下,我們可以避免這種情況和使用 hack 代碼。因為 hack 給了我們一種錯覺,讓我們覺得交付速度變快,至少一開始是這樣。但這是一種短期的投資,很快我們就會遭到回擊:代碼變得僵化,幾個月後,我們需要花更多的時間來理解和修改它們。即使是幾天後我們也可能不記得代碼做了什麼。這種情況會變得越來越糟,對於那些不清楚背景的新人來說就更是摸不著頭腦了。如果你的代碼庫中充斥著 hack,你可能不需要一直操心它們,因為它們可能不會存在太久。
你的測試套件中是否包含了大量的集成和端到端測試?
集成測試比單元測試更容易經受住變化,但是它們要慢得多。
如果你寫了很多端到端測試,那麼可能有過這些痛苦的經歷:
如果端到端測試涉及到通過網絡連接的許多組件(但請不要通過構建大泥球來避免這種情況),則會更加痛苦。
因此,糟糕的測試策略本身可能會影響你的交付速度。單元測試會「阻止」我們寫出更好的代碼,你的「敏捷」是否實現正確並不重要。你會感覺被穿上了一件束縛衣(或者不止一件,如果我們偏執地試圖通過增加越來越多的測試來達到 200% 的安全)。與此同時,那些能夠更快地適應市場變化的競爭對手正在向我們逼近,而束縛衣正把你裹得無法移動,對抵擋那些競爭對手一點用都沒有。
所以,我想讓你再問自己一些關鍵的問題:
一個例子
傳統的方式
我們來看一個常見的場景的簡化版本。一個使用 Spring 實現的後端應用程式,我通常會看到生產類和測試類之間的一對一映射,如圖所示:
「包含主要邏輯的類」通常被叫作「服務」。有時候服務是特定於每個領域實體,有時候代表某個更抽象的概念。
有時候,「主要邏輯」也會滲透到框架組件(如控制器或監聽器)中。不僅框架和業務邏輯之間的界限變得模糊,區分哪些可以在單元測試或集成測試中測試的界限也變得模糊。因此,有時候在單元測試和集成測試中會看到相似的場景,這是一種浪費。
如果類包含了複雜的功能,那麼每個類對應一個單元測試是有意義的。對於複雜的功能來說,識別出錯誤是很困難的,使用小段代碼有助於更快地定位問題,這實際上也是支持使用單元測試的原因之一。請記住,我說的是「複雜的功能」而不是「複雜的邏輯」,因為人們很可能通過複雜的方式實現簡單的功能。
問題來了
現在想像一下,由於需求發生了變化,你需要添加一個新特性或修改已有的邏輯。已有的實現不適合新的想法或環境,現在有兩種選擇:使用 hack 手段或重構。當然,我們總是選擇重構:
假設邏輯拆分得很乾凈(只是將一些方法移到新的類中),舊類保留對新類的引用。那麼我們有必要為了保持一對一的映射而對測試類進行拆分嗎?如果這樣做我們會得到什麼好處?當然,我們也可以保持測試類不變(只需要稍做修改)。
對於更複雜的重構,我們可能在 20 分鐘內就調整好了邏輯(實現細節)。但正如我們已經提到的,測試不會修改得這麼快。
對於測試,我們看到了兩點:
我們看到每種測試類型都有一些好處。因此,或許我們可以將集成測試和傳統的單元測試的優點結合起來。傳統的單元測試對應一個類或一個方法,至少很久以前是這樣的,似乎不對類進行單獨的測試就是不對的。但也許對「單元」的概念進行一番「重構」會更有意義。
有很長一段時間,我一直在想,為什麼我沒有看到人們談論這個問題。後來,我找到了一些討論這個話題的文章和視頻。在我閱讀 Vladimir Khorikov 的《單元測試原則、實踐和模式》一書時,我對這個問題和其他概念有了更清晰的了解。
如果單元不是對應類,那應該是什麼?一個跨了幾個方法或類的功能?你可能會說:「等等,那不就是集成測試?」好吧,不完全是。我借用書中的一些定義:
單元測試:
驗證一小段代碼;
快速完成;
用獨立的方式進行;
集成測試至少不滿足上述標準中的一個。
一個好的單元測試:
防止回歸;
不抗拒重構;
快速提供反饋;
易於維護。
單元測試:
驗證一小段代碼;
快速完成;
用獨立的方式進行;
集成測試至少不滿足上述標準中的一個。
一個好的單元測試:
防止回歸;
不抗拒重構;
快速提供反饋;
易於維護。
現在,「單元」的概念看起來更加抽象和靈活了。另外,對於集成測試,我們可以考慮很多種測試,比如系統測試、端到端測試等等。
一種更靈活的方式
現在我們進入更大的粒度級別,將初始的邏輯放到一個模塊中,並做一些修改:
我們先是將領域邏輯放到一個相對較小的模塊中。我們可以把模塊想像成微服務中的微服務。那麼模塊究竟有多小?對此並沒有簡單的規則可以遵循。請記住,這個示例是經過簡化的:可能涉及幾個實體,也可能沒有實體,並且可能有更多的相關類。
我們需要注意這裡的權衡:
優點:
代碼更加靈活。頻繁重新調整模塊內的邏輯不需要修改測試。
有內聚力的小塊內容比混亂的大塊內容更容易理解。這與微服務對大泥球之間的對比是一樣的。
單元測試已經測試了更多東西,因此需要的集成測試更少了。這給了我們:
更快的反饋和更短的構建時間。
單元測試比集成測試更容易調試,也更穩定。
清除了集成測試的障礙。業務邏輯只在單元測試中進行測試。
更少的處理過程,所以更加節省能源。
由於不需要模擬內部類,因此減少了工作量。
我們不需要模擬很多類,而是模擬模塊和框架組件,因此在測試時不需要啟動應用程式。
缺點:
需要注意的是,對非常複雜的功能進行單獨的測試可能是必要的。
我們做的第二件事是將領域與框架組件進行分離。
框架(如 Spring)的功能之一是將應用程式的不同元素粘合在一起。我們所要做的類似於六邊形架構,也就是埠和適配器:控制器、監聽器、過濾器、DAO 或其他框架構建塊是將領域邏輯(應用程式核心)連接到外部世界的埠。理想情況下,這些組件不包含領域知識。例如,控制器、監聽器和過濾器只包含對領域邏輯的調用。
優點:
分離領域和框架邏輯的一種擴展做法是將每個功能的領域邏輯放在一起,以實現內聚性。與每個功能相關的所有邏輯都在同一個模塊中實現,而不是分散在多個模塊中。
我已經經歷了一場噩夢,從一個單體代碼庫中找出所有需要修改的地方。我們密集調試了幾個月,而主要的改動在兩天內就完成了。
應用程式級別的集成測試
我們只需要對單元測試沒有覆蓋到的東西進行集成測試,比如其他框架功能:端點配置、序列化、數據和錯誤的反序列化、數據訪問、遠程調用、驗證。對正向路徑進行煙霧測試可能也會很有趣。
端到端測試
最後我想說的是端到端測試。端到端測試的成本比集成測試要高得多,所以要小心進行端到端測試。
對於非關鍵場景,可以考慮使用契約驅動測試來代替端到端測試。
改變習慣的策略
我記得這種方法在幾個案例中為我帶來了幫助,其中最重要的一個是我們為投標系統開發算法的案例。
算法的初始實現看起來還不錯,但在發布後我們每周都會發現一些棘手的情況。我們單獨對它進行修復,很快,算法變得複雜起來,並最終發生了一個事故。
在那之後,我們又檢查了一遍,找到了一個不一樣的方法。我們最終減少了 75% 的代碼,實現也更加簡單。
遺憾的是,單元測試單獨對這些方法進行了測試,而我們有一堆這樣的單元測試。如果我們將邏輯作為一個功能單元進行測試,可以節省 12 天(總共 15 天)的工作量,並避免因再次檢查所有測試用例而導致的挫折感。
所以,除了上述所有優點之外,這種測試方法還可以增加開發人員 / 團隊的幸福感。其他旨在更快交付的策略需要說服周邊的人,這可能非常困難。這一切都掌握在你的手中。
總的來說:
請記住:
我並不是說這種測試方式在任何時候對所有人都有好處,你需要評估這是否對你有益,以及在哪些情況下對你有益。正如之前所說的,它在敏捷環境中特別有用,因為在敏捷環境中會有很多實驗性的特性,領域是在疊代中不斷被創建和演化的,並且有規律地加入新的特性。
如果我們有意識地採用 TDD(測試驅動開發),也同樣可以獲得其中的一些好處。問題在於,我們總是傾向於過於關注工具或技術,而不了解其本質,忘記了初心,所以到最後我們一無所獲,並最終選擇放棄。
變化免疫力
要採用這樣的實踐,我們需要改變習慣,而要改變習慣非常困難。我們認為我們需要的是意志力,但這遠遠不夠。我想分享我最近發現的一種方法。哈佛大學教育研究生院成員 Lisa Lahey 博士和 Robert Kegan 博士在其合著的《對變化的免疫:如何克服它並釋放你自己和組織的潛力》一書中對其進行了描述。
我們常常試圖通過意志力來實現改變。他們的理論是,這可能不起作用,因為我們有一種類似於「免疫系統」的東西,它會抗拒變化,破壞我們所有的嘗試。因此,要接受改變,我們首先需要發現我們的「免疫系統」,並從根本上解決問題。
這種「免疫系統」是在過去逐步建立起來的。我們總是不斷地嘗試實現目標,而要實現這些目標,我們需要遵循一系列步驟:
這些概念構成了他們的方法論的基礎。我將這些步驟簡單地描述為:
如果我們發現這些假設不再有效,就繼續研究它們,朝著讓我們可以更容易接受變化的方向發展。
在播客「Dare to Lead」中,Lisa Lahey 和 Brené Brown 例舉了 Brown 在自己的生活中實踐「變化免疫力」的例子。播客分為兩個部分(第一部分和第二部分)。我經常會在做其他事情的同時收聽播客,但這個博客值得你認真聽。
播客以 Brown 提出的一個問題開始:「為什麼我們都想要發生轉變,但沒有人想要做出改變?」她們說,我們傾向於將改變的失敗與渴望不足或虛假的意圖聯繫起來。她們提到,意圖並不是萬能的,因為那些生命處於危險之中、想要活下去的人,有時仍然無法做出改變。
後來,她們提出了一個我們可能很熟悉的例子:Brown 說她希望團隊能夠更加自律地參加定期會議。
Lisa 用一些問題引導 Brown 填寫表格。對於 Brown 來說,這是讓她的生活變得更加輕鬆和取得成功的關鍵。這些原本是她自己可以改變的事情,但她卻無法實現改變。
當 Brown 發現這些沒有說出口的承諾、假設和擔憂正在破壞她並導致目前的窘況時,她感到很驚訝。她說,她從過去的經歷中學到,紀律和創造力是相互排斥的。因此,她認為定期開會會減少她做自己喜歡的(具有創造性的)事情的時間。所以她跳過了那些會議,結果是她開了很多一次性會議。她說,因為她想要表現出自己是一位平易近人的領導者,這幫她實現了很多目標,但現在她花了太多時間在這上面,她意識到定期開會並不會讓事情變得更糟,反而可以節省很多時間。
最後,Lisa 建議她想辦法接受這個新理念,即紀律和創造力是相容的。她需要建立新的神經通路,並覆蓋已經存在多年的舊神經通路。
我認為這個策略可以為我們帶來兩個方面的好處:這種來自我們「免疫系統」的破壞說明了我們在嘗試變得敏捷時做了什麼。我相信這可以應用到我們生活的許多方面。
例如,如果我們以切換到新測試策略為例,我們需要找出我們想要通過改變來實現什麼目標,以及為什麼它比其他事情更重要。對於我目前的情況來說,靈活性是至關重要的。
關於我們可能在做的與目標相悖的事情、我們為什麼要做它們以及我們做出的觸發這些活動的假設,我們可以認為,如果按照通常的方式去做可能會更容易,因為我們只需要慣性地繼續下去。我們也會找到不這麼做的理由:「這是我們一直以來的測試方式,我們就是被這麼教育出來的。所有人都在這麼做,所以它一定是對的。」
因此,為了對抗這種情況,我們需要承認負面影響,重寫那些信念,然後找到一種方法來證明這些假設是錯誤的,最後實現改變。
如果你想了解更多細節,可以查看上面提到的「Dare to Lead」播客(第一部分和第二部分)、他們的網站或著作。
原文連結:
https://www.infoq.com/articles/delivering-fast-testing-strategies/
融資 7 億元後,Mojo 之父實名吐槽:Mojo 太好用了,顫抖吧 C++
微軟被曝搪塞員工績效,只強化個人表現;文心一言 App 登蘋果免費應用排行榜首位;商湯科技被爆裁員?官方回應|Q資訊
一個潮流的終結?推出僅 3 年後,亞馬遜宣布終止低代碼 Honeycode 服務,前員工爆料:長期沒有顧客!
硬核探訪!AR 頭盔、數字孿生......揭秘寧德核電站的數字化實踐