自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

測試驅(qū)動開發(fā)(TDD)介紹中的誤區(qū)

開發(fā) 測試
通常的TDD介紹存在著本質(zhì)性的問題,它們通常會將學習者置于一個通往貌似目的地的道路上,但事實上并不能展示如何到達真正的目的地。這種現(xiàn)象太常見了,所以我決定給它取個名字:“現(xiàn)在該咋搞,朋友?”(WTF now, guys?)

目前我正在教授一個為期兩周的“敏捷開發(fā)實踐”速成課,參加培訓的團隊成員都是非常傳統(tǒng)的企業(yè)級Java開發(fā)者。要將社區(qū)中15年的進展?jié)饪s到8個半天的實踐課程中非常具有挑戰(zhàn)性:在嚴格地時間約束下,教授什么思想和實踐才能對這些開發(fā)者的職業(yè)生涯提供***幫助呢?

經(jīng)過幾天斷斷續(xù)續(xù)的思考,我至少得出了一個結(jié)論:傳統(tǒng)上會介紹給新人的測試驅(qū)動開發(fā)(TDD)將不會出現(xiàn)在我的課程里。

通常的TDD介紹存在著本質(zhì)性的問題,它們通常會將學習者置于一個通往貌似目的地的道路上,但事實上并不能展示如何到達真正的目的地。這種現(xiàn)象太常見了,所以我決定給它取個名字:“現(xiàn)在該咋搞,朋友?”(WTF now, guys?)

 

Fig. 1 — 說真的,到底什么情況??

我認為這種情況解釋了為什么開發(fā)者對TDD的看法存在如此多分歧。當某個開發(fā)者發(fā)出這樣的抱怨:“到處都是mock對象,太可怕了”,另一個正在攀登更高山峰的開發(fā)者可能會回復“?。康教幎际荕ock對象多好呀!”事實上,這是人們談?wù)揟DD時出現(xiàn)的典型情景,并且我相信出現(xiàn)這種問題的原因是:我們用相同的詞匯和工具描述完全不相關(guān)實踐。一個對于山峰前邊的人來說很合理的TDD問題,對于正在探索山峰另一邊的人來說可能完全是荒謬的。

如果我是對的(讀完下文后你可以自己判斷),我認為這個觀點揭示了為什么很多開發(fā)者曾經(jīng)對TDD的許諾和初步使用經(jīng)驗感到如此興奮,最終卻開始感到失望。

通過code katas(編碼實踐)教授“經(jīng)典TDD”

我們先來看看如何使用code katas教授TDD。

首先我會簡單演示一下如何通過測試驅(qū)動出一個返回任意斐波那契數(shù)的函數(shù)。我會不斷對自己說,“這一整天的例子盡管不那么實用,但至少可以用來說明‘紅燈-綠燈-重構(gòu)’的開發(fā)節(jié)奏”。稍后,我們會過一下Bob大叔的保齡球計分kata。當天培訓的***是由參加者自己結(jié)對實現(xiàn)一個羅馬數(shù)字到阿拉伯數(shù)組的轉(zhuǎn)換函數(shù)。

第二天,我會站在白板前讓同學們總結(jié)一下他們所體會的TDD的好處是什么。不出意外(但這點很重要),所有學員都將TDD看作是跟正確性相關(guān)的:“代碼沒有缺陷”、“自動化回歸測試代替手動測試”,“修改代碼不用擔心會改壞原有功能”等等。

當我對他們的回答評論說“TDD的主要好處是提高我們的代碼設(shè)計!”,他們顯得有些猝不及防。并且當我告訴他們,TDD所帶來的任何回歸測試安全性往好了說只是副作用,往壞了說可能只是個幻覺,他們開始左顧右盼,希望確保他們的老板沒有聽到我所說的。這聽起來可不像是他們最初希望得到的東西。

假設(shè)換種方式,我只是提供編碼練習,就像我每天所做的一樣,而忽略它們只是些簡單的練習題這一事實。當學生們發(fā)現(xiàn)他們在TDD編程練習中學到的經(jīng)驗對平時的工作毫無幫助時,他們會有多么失望。

錯誤#1:鼓勵龐大的代碼單元

對初學者來說,如果你的目標是讓每個測試都對解決你的問題有直接幫助,那么***你就會得到功能越來越多的代碼單元。***個測試將會得到一些直接解決問題的程序代碼。第二個測試會帶來更多。第三個測試會讓你的設(shè)計更加復雜。TDD實踐本身不會在任何時候告訴你需要改進實現(xiàn)本身的設(shè)計——將大段的代碼分割成小段。

 

 

Fig. 2 — 考慮上圖,如果出現(xiàn)一個新需求,大多數(shù)開發(fā)者都會想到在現(xiàn)有的單元上增加額外的復雜性,而不會預(yù)先想到新需求需要通過增加一個新的單元來實現(xiàn)。 

防止代碼設(shè)計變成一團亂麻變成了留給開發(fā)者的練習。這就是為什么很多TDD支持者要求在測試通過后增加一個“繁重的重構(gòu)步驟”,因為他們意識到需要在這個流程中對開發(fā)者進行干預(yù),以便讓他們能夠停一下,發(fā)現(xiàn)簡化設(shè)計的機會。

每次測試通過后進行重構(gòu)是TDD支持者的原則(畢竟要遵守“紅燈-綠燈-重構(gòu)”),不過在實踐中很多開發(fā)者經(jīng)常錯誤地的跳過這個步驟,因為TDD過程沒有任何內(nèi)在的規(guī)定強迫人們重構(gòu),直到***代碼變成一團亂麻。

一些培訓者會告誡開發(fā)者:嚴格的重構(gòu)才能體現(xiàn)紀律性與專業(yè)性的美德,希望以此來解決這個問題。這對我來說這并不能算是個解決方案。與其去質(zhì)疑那些做出巨大努力練習TDD的人們的職業(yè)素養(yǎng),我寧愿去質(zhì)疑在工具和練習的設(shè)計上是否能夠鼓勵人們在工作流中做正確的事。

錯誤2#:鼓勵費力的重構(gòu)提取操作

假設(shè)在代碼單元開始變得龐大時,你會主動進行提取的重構(gòu)操作。

 

 

Fig. 3 — 將單元的一部分職責提取到一個新的子單元中。不改變原始的測試以確保我們的重構(gòu)沒有破壞任何東西。

不過需要知道,提取重構(gòu)通常都會很痛苦。提取重構(gòu)通常需要仔細的分析以及全神貫注,這樣才能將一個復雜的父對象梳理為一個整潔的子對象和一個不那么復雜的父對象。引用Brandon Keeper所說的“把兩個毛線團打成一個節(jié),比把一個打了節(jié)的毛線團分成兩個毛線團要容易得多”。

錯誤3# 正確代碼的特性測試

即使重構(gòu)工作順利完成了,還有很多工作要做!為了保證系統(tǒng)中每個單元都有對應(yīng)的、設(shè)計良好的單元測試(我稱它為“對稱測試”),你需要設(shè)計新的單元測試來描述新的子對象行為。這種做法很有問題,因為特性測試是處理遺留代碼的測試工具,在真正的測試驅(qū)動開發(fā)中根本不應(yīng)當出現(xiàn)。同時,如果我們將“特性測試”定義為“為沒有測試的單元添加測試以驗證其行為”,這正是在描述我們所做的:為已經(jīng)實現(xiàn)的沒有相應(yīng)單元測試的單元編寫測試。

因為新的測試并不是用正常的TDD節(jié)奏編寫的,開發(fā)人員面臨著與“實現(xiàn)后添加測試”情況同樣的風險。也就是說,因為代碼已經(jīng)存在了,你的特性測試無法確保驗證到了新的子單元的全部行為。所以,即使你為了覆蓋新的單元,做了這么多額外的(也是值得稱贊的)工作,能達到的測試質(zhì)量上限也始終比從頭進行測試驅(qū)動開發(fā)要低。這個結(jié)果說明了這種活動其實是種浪費。

 

 

Fig. 4 — 為新的子單元行為添加特性測試。我們需要對測試的健壯性持謹慎的態(tài)度,因為它是“開發(fā)后添加測試”的產(chǎn)物。

錯誤4#  冗余的測試覆蓋

但是現(xiàn)在你的系統(tǒng)面臨著另一個測試陷阱:冗余的測試覆蓋!同一行為在兩個地方都被覆蓋到對于TDD新手來說可能感覺很舒適,直到改動成本開始變得失控。

假設(shè)來了一個新的需求要求改動提取出來的子對象行為。理想情況下,這需要做三處改變(這三點是開發(fā)者都能夠預(yù)測到的):驗證新特性的集成測試、描述新行為的單元測試、以及單元代碼本身。但是在我們的冗余測試例子中,父單元的測試同樣需要做出修改。

更糟糕的是,實現(xiàn)這個變更的開發(fā)者根本想不到父對象的單元測試會失敗。也就是說,***的情況是,開發(fā)者面臨一個意想不到的“驚喜”:父單元的測試失敗了,需要額外的精力根據(jù)子對象的行為去重新設(shè)計父單元的測試。最差的情況可能是,開發(fā)者可能沒有意識到這個測試失敗其實是一個由于業(yè)務(wù)改變而導致的誤報,并不是一個真實的bug,這會導致大量時間耗費在發(fā)現(xiàn)父單元測試的失敗原因上。

 

 

Fig. 5 — 子對象的修改導致父對象的測試失敗,需要重新設(shè)計父對象的測試,即使父對象本身并沒有修改。

假設(shè)子對象被用在兩個地方——甚至10個地方!一個被依賴單元的簡單修改就會導致對依賴單元數(shù)小時的痛苦測試修復工作。

錯誤5#  以犧牲回歸有效性來消除冗余

如果我們希望避免冗余測試最終所帶來的痛苦,那么實現(xiàn)一個簡單的提取方法的重構(gòu)就要求我們重新設(shè)計父單元的測試。

要知道父單元的測試原本是有正確性和回歸安全性保證的,所以原來的作者可能并不喜歡我為了移除冗余所做的事——把父單元中的子單元實例替換成它們的測試替身。

 

 

Fig. 6 —將父單元測試由原來的使用真實子單元實例替換成測試替身。

“現(xiàn)在這些測試就沒什么意義了,它們實際上驗證不了任何事!”最初的作者可能會這么說。根據(jù)當初編寫這些代碼的本意來說(TDD就是迭代式的解決問題,同時保證了完全的回歸安全),他們的意見是絕對正確的??梢赃@樣反駁他們的觀點“可是這些單元已經(jīng)有獨立的測試了”,但是因為缺少額外的集成測試確保這些單元協(xié)同工作的正確性,原作者的擔心并不是沒有道理的。

在這一點上,我見多過很多團隊進入死胡同,一些人會很喜歡使用mock,另一些人則十分反對mock,但是沒有人真正理解這種爭論只是一種表象,它的根源是經(jīng)典TDD給我們提供的錯誤假設(shè)。

錯誤6# 濫用Mock

雖然我通常會推薦團隊使用mock,但他們像如下這樣使用mock并不是個好主意。首先,將子單元替換成測試替身將會導致父單元的測試復雜化:測試代碼的一部分會表述父單元的邏輯行為,另一部分則會描述預(yù)期中的父單元與子單元協(xié)作方式。除了要處理以上的兩方面的內(nèi)容,測試還綁定了父子單元如何協(xié)作的細節(jié),因為任何調(diào)用都必須與父單元的實現(xiàn)邏輯相配套。

像這樣即描述了邏輯行為又描述了單元協(xié)作過程的測試是很難閱讀、理解與修改的。并且這種恐怖的情況可能遍及使用測試替身的大多數(shù)測試之中。這就難怪我總是聽到單元測試里有太多mock的抱怨,最近這個問題也很讓我困擾。

要解決這種濫用問題工作量也很大。父單元需要做重構(gòu),讓它只是引導其他單元的協(xié)作而本身不含有任何實現(xiàn)邏輯。這就要求父單元里那些之前沒有提取到子單元中的行為現(xiàn)在需要被抽取到另一個新的單元中(包括目前為止討論到的所有耗時的活動)。最終,父單元原始的測試將被拋棄,新的測試將只包含關(guān)于協(xié)作的描述,確保各子單元之間的交互是必須的。哦,由于現(xiàn)在完全沒有集成測試確保父單元工作正常,我們還需要添加一個集成測試。

 

 

天那,使用這套方法需要這么多精力以及紀律性才能維護一套整潔的代碼、可理解的測試、以及迅速的構(gòu)建,這就難怪很少有團隊最終達到使用TDD所希望達成的目標。

#p#

成功應(yīng)用TDD的方式

因此,我希望提供一個全新的課程,引入與上文描述完全不同的TDD工作流。

首先,考慮一下上文所述的痛苦曲折過程的最終產(chǎn)物:

一個父單元,依賴兩個子單元實現(xiàn)邏輯功能

父單元的測試,描述了兩個子單元的交互

兩個子單元,每個都有單元測試描述他們各自的職責

如果這就是我們要達到的最終目標,為什么不在最開始就朝著這個方向前進?我的TDD方法考慮到了這一點,并且可以做為簡化論的一個應(yīng)用。

我的流程是這樣的:

(1) 拉入一個新的特性需求,這要求系統(tǒng)完成一些新的功能。

 

 

(2) 為特性看起來的復雜性感到恐慌。思考自己為什么一開始干上了編程這份工作。

 

 

(3) 為特性找到一個切入點,從建立一個公共接口契約開始(例如:“我會為控制器增加一個行為,返回給定年月的利潤值”)

 

 

此時也是將公共契約寫入集成測試的好機會。本文并不是關(guān)于集成測試的,不過我推薦運行于自己獨立進程的測試,它可以像真實用戶那樣與應(yīng)用交互(例如通過HTTP請求)。如果從一開始就加入集成測試保證回歸安全性,我們的單元測試就不需要太多考慮集成測試的問題了。

(4) 為切入點編寫單元測試,不過不需要嘗試立刻去直接解決問題,要有意識地延遲編寫實現(xiàn)邏輯!應(yīng)當這樣,假想你已經(jīng)有了所需要的一些對象,通過這種方式來化簡問題(例如“如果這個控制器只依賴一個根據(jù)月份獲取收入的對象和一個根據(jù)月份獲取開支的對象,那一定很簡單”)。

 

 

由于這個步驟本身就鼓勵使用小型、單功能的單元,因此它能改善你的設(shè)計。

(5) 用TDD的方式實現(xiàn)切入點代碼,編寫測試就像那些假想的單元已經(jīng)存在了。在切入點要用到的依賴對象處注入測試替身,在測試中描述它和依賴之間的交互。交互測試描述那些“協(xié)作”單元,它們只負責控制其他單元的使用,本身不包含邏輯。

這一步能夠改善你的設(shè)計,因為它給你機會去發(fā)現(xiàn)新依賴所必須擁有的API。如果某個交互很難測試,那么修改函數(shù)簽名會很容的,因為這些依賴目前還沒有實現(xiàn)。

 

 

(6) 對每個新想到的對象重復步驟4和5,發(fā)現(xiàn)更多更小粒度的協(xié)作對象。

受人類天性影響,人們在這一步時可能會感到比較恐慌(“這樣我們會得到無窮多微小的類!”),但在實踐中通過很好的代碼組織,這是可以管理的。由于每個對象都很小、容易理解、用途單一,因此通常由于需求變化而要刪除某個不再使用的單元,或是對象體系中的一整個子樹的對象并不會帶來很大痛苦。(我曾經(jīng)遇到過 一份很不幸的代碼,里邊有很多龐大的、被到處使用的對象,因而基本上不可能刪除它們,即使它們已經(jīng)不再符合當初的設(shè)計初衷了)。

 

 

(7) 最終,直到工作再無法細分。此時,在這些對象圖中處于葉子節(jié)點的對象中實現(xiàn)***的一點邏輯,之后重新回到樹的頂端開始下一個修改。

 

 

這個過程的目標就是發(fā)現(xiàn)盡可能多的協(xié)作對象,這樣就可以保證葉子節(jié)點只需要實現(xiàn)最簡單的邏輯。

“邏輯單元”的測試會詳盡得描述有效行為,并且可以讓作者有理由相信單元測試是完備與正確的。邏輯單元的測試可以保持簡單,因為沒有必要使用測試替身——只需要根據(jù)不同的輸入驗證對應(yīng)的輸出結(jié)果。

我喜歡把這種過程成為“Fake it until you make it”(使用偽對象,直到你實現(xiàn)它),雖然這其實是來自GOOS一書中的敏銳觀點,但它更強調(diào)了簡約化。我還發(fā)現(xiàn)區(qū)分“協(xié)作單元”與“邏輯單元”是很有價值的,不但能夠使測試更清晰而且也增加了代碼的一致性。

同時注意到采用這種TDD方式不需要加入繁重的重構(gòu)步驟。提取重構(gòu)操作成為一種特例而不是常規(guī)行為,這就意味著上文詳細描述的提取重構(gòu)帶來的后續(xù)附加成本能夠完全避免。

改變我們教授TDD的方式

我用了四年時間才完全理解我使用TDD所遇到的挫折,并將這些思考寫成了這篇文章。經(jīng)過對這些問題長時間的徘徊與思索,我可以說最終我認為TDD是一個高效的、愉快的實踐。不值得為所有的項目嘗試投入那么多TDD時間,但在構(gòu)建一個預(yù)期會生存很長時間的系統(tǒng)時,TDD是能夠幫助我們戰(zhàn)勝焦慮和復雜性的一個有效工具。

我將這些分享給大家的目的是告訴大家這才是真正的測試驅(qū)動開發(fā)。經(jīng)典TDD的簡單假設(shè)給新人帶來的痛苦并不能讓他們學到太多東西。讓我們一起找到一種方法能夠?qū)⒏行У腡DD工作流教授給學員,讓他們可以立刻使用這些有效的工具將令人困惑的大問題拆分成為可以控制的小問題。

原文鏈接: testdouble   翻譯: 伯樂在線 治不好你我就不是獸醫(yī)

譯文鏈接: http://blog.jobbole.com/64431/

 

責任編輯:林師授 來源: 伯樂在線
相關(guān)推薦

2009-10-10 10:55:48

TDD技術(shù)

2023-09-11 11:05:49

軟件開發(fā)TDD

2021-02-21 08:53:19

測試驅(qū)動技術(shù)數(shù)據(jù)驅(qū)動pytest

2021-02-04 07:30:14

測試驅(qū)動技術(shù)excel讀取數(shù)據(jù)

2021-02-04 07:12:15

測試excelapi

2021-01-19 07:46:48

TestNG測試驅(qū)動TDD

2009-07-08 09:44:54

TDDViual Studi

2011-07-14 14:15:40

ThreadLocal

2023-02-23 19:28:09

ODD測試

2009-08-26 16:25:43

軟件測試

2011-07-04 17:09:54

2012-10-30 15:53:29

測試測試驅(qū)動開發(fā)驅(qū)動開發(fā)

2023-07-12 13:08:58

性能測試數(shù)據(jù)

2022-05-27 12:40:25

前端測試項目

2011-07-04 18:12:09

功能測試故障模型

2011-06-13 10:28:45

JAVA

2017-03-16 13:17:54

TDD代碼開發(fā)

2010-01-04 17:34:15

Ubuntu gstr

2013-02-21 09:38:48

測試軟件測試測試驅(qū)動

2020-09-10 07:00:00

測試工作測試實踐
點贊
收藏

51CTO技術(shù)棧公眾號