背景:
- 重新閱讀了以下 可以引發(fā)思考 的 陳年老文
之所以要 “叕” 談 TDD, 除了上述背景,也是因?yàn)樽约汗ぷ?年來,雖然經(jīng)常聽到 TDD,但著實(shí)沒有“完整” 的在項(xiàng)目上實(shí)踐過它。直到最近打算在當(dāng)前的交付項(xiàng)目上實(shí)踐,才又重新審視 這項(xiàng)實(shí)踐,以求回答下列問題:
在逐一回答這些問題之前,先說我對 TDD 這種實(shí)踐的 觀點(diǎn):
- TDD 是確保 Dev 在編寫代碼時,處于 對需求保持 “清醒(Obvious)” 狀態(tài) 的方式之一,但并非 唯一 方式
- TDD 中的測試(T)要面向業(yè)務(wù)需求,而非代碼實(shí)現(xiàn)
- TDD 是一種 快速, 可復(fù)用 的 反饋獲取 方式,而非唯一方式
- 如果能 不用 TDD 并做到上述 3 點(diǎn),那么不 TDD 也沒問題。
為何 TDD
關(guān)于這一問題,在我剛開始接觸 TDD 實(shí)踐理論時,一定會 ”張口就來“: 為了產(chǎn)品代碼的質(zhì)量有所保證。可是,多想一步,只要有充分且有效的自動化測試,甚至足夠的手工測試,產(chǎn)品代碼的質(zhì)量就會有所保證的呀,這和 TDD 有什么關(guān)系?
那么,我們?yōu)槭裁匆?TDD 呢?
思考再三,還是逃不脫 “為了產(chǎn)品代碼的質(zhì)量有所保證”,只不過,這是根本目的。從根本目的出發(fā),要最終能推導(dǎo)出使用 TDD,就需要引入其他考慮因素:
- 測試先行 對 測試“性價比”的影響
- TDD對認(rèn)知狀態(tài)的影響
測試先行 對 測試“性價比”的影響
軟件質(zhì)量測試的形式、種類是多種多樣的,其測試粒度也不全相同。通常,測試粒度越細(xì),構(gòu)建、運(yùn)行越快,自動化程度越高,測試成本也就越低;反之,則成本越高。測試金字塔對此進(jìn)行了詳細(xì)的說明。在此不做展開。

那么,為了能夠降低測試成本,通常軟件開發(fā)過程中,都會選擇用更多的金字塔低層類型測試(單元測試、集成測試等)來盡量覆蓋足夠多的產(chǎn)品代碼。
那么問題就會變成:在完成產(chǎn)品代碼的開發(fā)后,將對應(yīng)的測試代碼都補(bǔ)上,不就可以達(dá)到與測試驅(qū)動開發(fā)同樣的效果了嗎?
其實(shí)上述問題建立在一個假設(shè)之上:先于產(chǎn)品代碼寫出的測試代碼 與 后于產(chǎn)品代碼寫出的測試代碼 的測試效果(價值)是相同的
在確認(rèn)這個假設(shè)是否成立之前,我們可以先來進(jìn)行一個嘗試:
需求: 張三有個賬本,他每天都會記賬,記賬的形式包含日期,款項(xiàng),數(shù)額。收入會被記為正數(shù),支出會被記為負(fù)數(shù)。單位均為人民幣單位“元”。這天,他突然想知道上一自然周(周一到周天)的支出總數(shù),你可以幫助他完成計(jì)算嗎?
- 代碼庫
- 用下列兩種方式完成嘗試:
- 先寫邏輯,后補(bǔ)測試
- 先寫測試,再寫邏輯
----------------------------------------------建議先花些時間嘗試后,再向下閱讀--------------------------------------
接下來,我會將自己按照上述兩種方法的思考和完成歷程展示在這里:
先寫邏輯(源碼)
-
讀代碼,很容易可以鎖定新的邏輯應(yīng)該在
LedgerService中添加,這里應(yīng)該需要一個名為GetLastWeekCost的,無參的,public方法,該方法返回上周開銷,類型為decimal。 -
分析需求并拆分:上一個周一到周天的支出總數(shù)
- 實(shí)現(xiàn)找到不早于上一個周一日期的
Transaction的邏輯 - 實(shí)現(xiàn)找到不晚于上一個周天日期的
Transaction的邏輯 - 找出其中的支出項(xiàng)(
Amount < 0) - 將上述邏輯組合,作為過濾條件,找到符合時間范圍的
transactions,對其Amount求和。
- 實(shí)現(xiàn)找到不早于上一個周一日期的
-
寫邏輯代碼,按照上述拆分后的需求邏輯實(shí)現(xiàn)代碼,如下圖:
先寫邏輯
-
補(bǔ)充測試代碼,由于已經(jīng)完成了邏輯代碼,此時應(yīng)該已經(jīng)清楚需要考慮的邊界條件,故在構(gòu)建測試數(shù)據(jù)時,會將邊界外,和邊界上的情況都考慮在內(nèi),那么測試就會是這樣:
后補(bǔ)測試
至此,看上去一切順利。順帶問一句,你也是這樣做的嗎?
先寫測試(源碼)
-
讀代碼,很容易可以鎖定新的邏輯應(yīng)該在
LedgerService中添加,這里應(yīng)該需要一個名為CalculateLastWeekCost的,無參的,public方法,該方法返回上周開銷,類型為decimal。 -
分析需求并拆分:上一個周一到周天的支出總數(shù)
- 實(shí)現(xiàn)找到不早于上一個周一日期的
Transaction的邏輯 - 實(shí)現(xiàn)找到不晚于上一個周天日期的
Transaction的邏輯 - 找出其中的支出項(xiàng)(
Amount < 0) - 將上述邏輯組合,作為過濾條件,找到符合時間范圍的
transactions,對其Amount求和。
- 實(shí)現(xiàn)找到不早于上一個周一日期的
目前為止,我的思路都和先寫邏輯是一樣的。接下來,我們要先寫測試了,那么我們要測試什么呢?
按照步驟 2中拆分的需求,我們應(yīng)該分別測試:
- 找到不早于上一個周一日期的
Transaction的邏輯 - 找到不晚于上一個周天日期的
Transaction的邏輯 - 找到其中支出項(xiàng)
Transaction的邏輯 - 上述條件進(jìn)行組合后,完成求和的邏輯
這樣一來,步驟1中設(shè)想的方法CalculateLastWeekCost將會是上述邏輯組合后的結(jié)果,而要測試上述的幾步,需要有另外的public方法才能將其暴露給測試完成檢測,即在實(shí)現(xiàn)步驟1中的方法前,應(yīng)該有另外一個GetLastWeekTransactions的方法,用于校驗(yàn)Transaction的獲取邏輯無誤。
-
將預(yù)設(shè)的代碼結(jié)構(gòu)初步呈現(xiàn)出來,如下:
初步構(gòu)想
ps:
- 當(dāng)在同一個類中,一個
public方法依賴另一個public方法時,類的 單一職責(zé)原則 就被破壞了,這里可以記下一個 Tech Debt,以便后續(xù) 重構(gòu) - 理論上,
CalculateLastWeekCost方法中,在這一階段(還沒寫測試),不應(yīng)有任何實(shí)現(xiàn),但由于此時,分析的很充分,并且該邏輯相對簡單,故提前寫上了??
- 按照拆分的需求,開始構(gòu)建測試,并由測試驅(qū)動實(shí)現(xiàn)邏輯:
4.1. 測試 找到不早于上一個周一日期的Transaction的邏輯, 如下:
測試先行
為了讓測試通過,需要補(bǔ)上相應(yīng)的邏輯實(shí)現(xiàn),如下:
實(shí)現(xiàn)對應(yīng)的邏輯
4.2. 測試 找到不晚于上一個周天日期的Transaction的邏輯, 如下:測試先行
要通過測試,就需要對應(yīng)的邏輯實(shí)現(xiàn),如下:
實(shí)現(xiàn)對應(yīng)的邏輯
4.3. 測試 找到其中支出項(xiàng)Transaction的邏輯, 無需新加測試用例,可以在原有的用例上,對方法名和期望進(jìn)行修改,如下:
修改后的用例1
對用例2 也需要完成相似的修改,之后,補(bǔ)充對應(yīng)的實(shí)現(xiàn)邏輯,如下:
補(bǔ)充實(shí)現(xiàn)
4.4. 測試 上一周的支出總和,也就是CalculateLastWeekCost方法,如下:
相應(yīng)的測試 - 考慮到之前我們有發(fā)現(xiàn) Tech Debt,故接下來,需要進(jìn)行重構(gòu)。但本文暫時對于重構(gòu)不做討論。故可以認(rèn)為 “先寫測試” 但這里就結(jié)束了。
那么,先寫測試,你也是這樣做的嗎?
小結(jié):
至此,可以看出之前提到的假設(shè):
先于產(chǎn)品代碼寫出的測試代碼 與 后于產(chǎn)品代碼寫出的測試代碼 的測試效果(價值)是相同的
多半是很難成立的。
- 先寫邏輯:
- 流程相對較短,代碼量相對少,實(shí)現(xiàn)起來也相對快
- 后補(bǔ)的測試代碼的粒度相對較粗
- 一些 Tech Debt 可能被會隱藏起來
- 先寫測試:
- 流程相對較長,代碼量相對多,實(shí)現(xiàn)起來也相對慢
- 先寫測試,測試的粒度與需求的拆分粒度相關(guān)
- 測試與需求的映射關(guān)系相對明顯
- 由于測試本身的限制(只能測試
public方法),會由此出發(fā)開發(fā)者設(shè)計(jì)出更易于測試的方法(接口),從而將某些隱藏的Tech Debt 暴露出來
那么再回到 測試“性價比” 這個點(diǎn),在軟件開發(fā)領(lǐng)域,成本可以用“人/天”來衡量,也就是工作量。從上面的小結(jié)來看,先寫測試與先寫邏輯,是可能在工作量上產(chǎn)生差別的,也就是成本會不同。但同時,不同成本,帶來的回報也是不同的。
如果一個項(xiàng)目,更看重的是代碼的整潔度,也有時間打磨代碼質(zhì)量,那么先寫測試的“性價比”是更高的;否則,先寫測試可能無法帶來其他的附加價值以抵消它同時帶來的成本增加。
TDD對認(rèn)知狀態(tài)的影響
Cynefin 認(rèn)知模型 通常被用來描述人類對于某項(xiàng)有序事物的認(rèn)知程度。軟件開發(fā)過程中遇到的大多問題都是有序的,或者有規(guī)律可循的,那么對于這些具體問題(需求)的認(rèn)知階段也就應(yīng)該符合認(rèn)知模型所描述的變化過程:

理想情況下,當(dāng)開發(fā)人員對問題的解決方案處于“清楚,明確”的認(rèn)知狀態(tài)下時,編寫的產(chǎn)品代碼所包含的Bug應(yīng)當(dāng)是最少,甚至沒有的。
如何將開發(fā)人員的狀態(tài)向這種理想狀態(tài)調(diào)整,最簡單直接的辦法就是"多做幾遍"
- 第一遍,盡量探索發(fā)現(xiàn)問題
- 第二遍,嘗試解決所有問題
- 第...遍,解決所有問題,達(dá)成目標(biāo)
但是,顯然軟件開發(fā)、交付的過程中,不可能總將同一功能做多次,也上線多次。那么如何讓開發(fā)者向“清楚、明確”的狀態(tài)調(diào)整呢?
傳統(tǒng)的“瀑布”工作流程給出了一種解決方式:詳盡的需求分析文檔,以及詳細(xì)的設(shè)計(jì)文檔。(本文不做展開)
另外與之對應(yīng)的“敏捷”工作流程也給出了一種解決方式:測試驅(qū)動開發(fā)(TDD)。
那么 TDD 是如何將開發(fā)者的認(rèn)知向“清楚、明確”的狀態(tài)調(diào)整的呢?主要是靠這兩種 TDD 中隱含的實(shí)踐:
- Tasking(任務(wù)拆分)
- Fast Feedback (快速反饋)
在嘗試解決上一小節(jié)中的問題時,我們都先完成了需求的拆分。當(dāng)開發(fā)者將一個內(nèi)容含量較多且復(fù)雜的問題,拆解成一個個的小問題時,Ta 的認(rèn)知就會在這個過程中發(fā)生變化?;叵肷弦恍」?jié)中的需求:
計(jì)算張三上一自然周(周一到周日)的支出總和
為了便于測試,我們將它拆分成了:
- 實(shí)現(xiàn)找到不早于上一個周一日期的
Transaction的邏輯- 實(shí)現(xiàn)找到不晚于上一個周天日期的
Transaction的邏輯- 找出其中的支出項(xiàng)(
Amount < 0)- 將上述邏輯組合,對最終過濾出的
Transactions求和
這其實(shí)就是簡化版的Tasking。
而在后續(xù)的實(shí)現(xiàn)過程中,通過運(yùn)行并觀察測試結(jié)果(紅或綠),就可以更快速的獲取當(dāng)前的產(chǎn)品代碼是否滿足需求的校驗(yàn)結(jié)果,從而基于結(jié)果進(jìn)行代碼(認(rèn)知)的校準(zhǔn)。這也就是 Fast Feedback。
你可以嘗試回憶和思考,每次當(dāng)你寫完一個測試用例,再去寫對應(yīng)的實(shí)現(xiàn)時,對于將要編寫的產(chǎn)品代碼的認(rèn)知程度是怎樣的呢?
總結(jié)
為何 TDD ?
TDD 可以通過做到下列幾點(diǎn):
- 測試與需求的映射關(guān)系相對明顯,測試可以作為需求文檔便于后人維護(hù)、學(xué)習(xí)。
- 由于測試本身的限制(只能測試
public方法),會由此出發(fā)開發(fā)者設(shè)計(jì)出更易于測試的方法(接口),從而將某些隱藏的Tech Debt 暴露出來。 - TDD 會間接強(qiáng)制開發(fā)人員進(jìn)行 Tasking,拆分復(fù)雜問題為多個相對簡單的問題,讓開發(fā)者的認(rèn)知狀態(tài)發(fā)生變化。
- TDD 提供了快速獲取反饋的途徑,從而讓開發(fā)者能高效的進(jìn)行認(rèn)知的校準(zhǔn)。
從而使產(chǎn)品代碼的質(zhì)量有所保證。
下一篇《“叕”談 TDD(三)--- 如何TDD》將詳細(xì)說明 Tasking 的方式,以及如何進(jìn)行TDD。









