今天我們來談一談TDD 和 BDD 兩項實踐。我們先來說說 TDD,也就是測試驅動開發(Test Drvien Development)。 TDD 的節奏 或許你已經迫不及待地要舉手了:“TDD 我知道,就是先寫測試,後寫代碼。”但真的是這樣嗎?嚴格地說,“先寫測試、後寫代碼”的做法叫測試先行開發( ...
今天我們來談一談TDD 和 BDD 兩項實踐。我們先來說說 TDD,也就是測試驅動開發(Test Drvien Development)。
TDD 的節奏
或許你已經迫不及待地要舉手了:“TDD 我知道,就是先寫測試,後寫代碼。”但真的是這樣嗎?嚴格地說,“先寫測試、後寫代碼”的做法叫測試先行開發(Test First Development),而不是測試驅動開發。
測試驅動開發不也是先寫測試後寫代碼嗎?二者之間有什麼區別呢?
要回答這個問題,我們需要知道 TDD 的一個關鍵要素, TDD 的節奏:紅-綠-重構。
紅表示寫了一個新的測試,測試還沒有通過的狀態;綠表示寫了功能代碼,測試通過的狀態;而重構就是在完成基本功能之後,調整代碼的過程。
這裡說到的紅和綠源自單元測試框架。因為在很多單元測試框架運行測試的過程中,測試不過時會用紅色展示測試結果,而通過時則採用綠色進行展示,這已經成了單元測試框架約定俗成的規則。
讓單元測試框架流行起來的是 JUnit,其作者之一是 Kent Beck。TDD 走進大眾視野則依賴於極限編程這個軟體工程方法論的興起,而極限編程的創始人也是 Kent Beck。Kent Beck 在 JUnit 和 TDD 兩件事都有著重大貢獻,也就不難理解為什麼 TDD 的節奏叫“紅-綠-重構”了。
先寫測試,然後寫代碼完成功能,在第一步和第二步上,測試先行開發和測試驅動開發是一樣的。二者的差別在於,測試驅動開發並沒有就此打住,它還有一個更重要的環節:重構(refactoring)。
也就是說,在功能完成而且測試跑通之後,我們還會再次回到代碼上,處理一下代碼中寫得不理想的地方,或是消除新增代碼與舊有代碼之間的重覆。你或許會問,那為啥不在第二步“綠”的時候就把代碼寫好呢?因為“綠”的關註點只是讓測試通過,把功能完成。
所以我們說, 測試先行開發和測試驅動開發的差異就在重構上。
很多人只記住了“先寫測試,後寫代碼”,因為在很多人的印象中,寫代碼唯一重要的事就是完成功能。通過了測試,就是完成了功能,也就意味著萬事大吉了。然而,這種想問題的方式會讓人忽略新增代碼可能帶來的 壞味道(Code Smell),壞味道會讓代碼逐漸腐壞,這是一個工程問題,也就是會有長期影響的問題。
人的註意力是有限的,讓人在一個階段把所有事情都做好,很難。事實上我們會看到,很多團隊代碼變亂的一個重要原因就是把全部的註意力都放到完成功能上,根本無暇顧及代碼本身的質量。從這個角度上看,TDD 是更符合人性的做法,它把完成功能和代碼調整當成了兩個階段。
重構就是一個消除代碼壞味道的過程。一旦你有了測試,你就可以大膽地重構了,因為任何修改錯誤,測試都會替你捕獲到。
在測試驅動開發中,重構與測試是相輔相成的:沒有測試,修改代碼只能是提心吊膽;沒有重構,代碼的混亂程度會逐步增加,測試也會變得越來越不好寫。
現在,你已經理解了測試驅動開發不只是“先寫測試,後寫代碼”。但這隻是破除了概念上的誤區,我們還需要再進一步,知道測試怎麼“驅動”開發。
測試“驅動”開發
不難理解,重構和測試相互配合,這個過程就會“驅動”著我們把代碼寫得越來越好。不過,這隻是對“驅動”一詞最粗淺的理解。
首先,我來問你一個問題,測試驅動開發,從哪裡開始呢?很多人會說,測試驅動開發不是從測試開始的嗎?這個答案非常直觀,我們可以接著追問下去,寫測試要從哪裡開始呢?
對很多人來說,TDD 是一種難以接受的做法,拋開理念上的差異,更重要的原因是,寫測試無從下手。很多時候寫不出測試,主要是面對的需求太大了。所以,真正動手做開發的第一步是任務分解,把一個規模很大的需求拆分成若幹小任務。面對一個具體的小任務,我們才有動手寫測試的基礎。測試驅動開發要從任務分解開始。
具體到了寫測試的環節,即便面對的是一個小任務,對很多人來說,這依然不是一件容易完成的事。同樣,我們在前面分析過,想要寫出測試,需要有可測試的代碼。這意味著,我們的代碼需要有一個可測試的設計。如果不能寫測試,我們就要調整代碼,讓代碼變得可以測試,這是我們上一講中談遺留系統測試所講的內容。
從這裡你可以看出,從測試出發考慮問題的這種思考方式,會徹底顛覆掉我們原有的工作習慣,甚至是為了測試調整設計。但結果是我們得到了一個更好的設計,所以,很多懂 TDD 的人會 把 TDD 解釋為測試驅動設計(Test Driven Design)。
現在你可以理解了, 為了寫測試,首先“驅動”著我們把需求分解成一個一個的任務,然後會“驅動”著我們給出一個可測試的設計,而在具體的寫代碼階段,又會“驅動”著我們不斷改進寫出來的代碼。把這些內容結合起來看,我們真的是在用測試“驅動”著開發。
TDD 這麼好,為什麼行業里採用 TDD 這種工作方式的人並不多呢?首先,很多人本身對 TDD 的理解是錯誤的,這是我在前面分析過的;其次,TDD 看似簡單的節奏中,其實需要很多前置的基礎,比如任務分解、可測試的設計等等,而這些能力是很多人不具備的。換個角度看,TDD 只是冰山一角,露在海面之上的是 TDD 的節奏,而藏在海面下的是任務分解、軟體設計這些需要一定時間積累的能力。
前面說過 TDD 是來自極限編程,那極限編程為什麼要叫極限編程呢?
極限編程之所以叫“極限”,它背後的理念就是把好的實踐推向極限:
-
如果集成是好的,我們就儘早集成,推向極限就是每一次修改都集成,這就是持續集成。 -
如果程式員寫測試是好的,我們就儘早測試,推向極限就是先寫測試,再根據測試調整代碼,這就是測試驅動開發。 -
如果代碼評審是好的,我們就多做評審,推向極限就是隨時隨地代碼評審,這就是結對編程。 -
如果客戶交流是好的,我們就和客戶多交流,推向極限就是客戶與開發團隊時時刻刻在一起,這就是現場客戶。
極限編程本身的實踐值得我們好好學習,但極限編程背後這種理念其實也非常值得我們學習。我們在日常工作中也不妨多想想, 有哪些做法是好的,如果把它推向極致會是什麼樣子。 這種想問題的方式會在很大程度上拓寬你的思路。
說完了TDD,那什麼是BDD呢?我們都知道,在軟體開發中最重要的一個概念就是分層,也就是在一些模型的基礎上,繼續構建新的一些模型。程式員最耳熟能詳的分層概念就是網路的七層模型,只要一層模型成熟了,就會有人基於這個模型做延伸的思考,這樣的做法在測試上也不例外。
當 JUnit 帶來的自動化測試框架風潮迅速席卷了整個開發者社區,成了行業的事實標準,就開始有人基於測試框架的模型進行延伸了。各種探索中,最有影響力的就是 BDD。
行為驅動開發
BDD 的全稱是 Behavior Driven Development,也就是 行為驅動開發。BDD 這個概念是2003年由 Dan North 提出來的。
單元測試框架寫測試的方式更多的是面向具體的實現,這種做法的層次是很低的,BDD 希望把這個思考的層次拉高。拉到什麼程度呢?軟體變化的源動力在業務需求上,所以,最好是能夠到業務上,而校驗業務的正確與否的就是業務行為。這種想法很大程度上是受到當時剛剛興起的領域驅動設計(Domain Driven Design)中通用語言的影響。在 BDD 的話語體系中,“測試”的概念就由“行為”所代替,所以,這種做法稱之為行為驅動開發。
Dan North 不僅僅提出了概念,而且為了踐行他的想法,他還創造了第一個 BDD 的框架:JBehave。後來又改寫出基於 Ruby 的版本 RBehave,這個項目後來被併到 RSpec 中。
好,瞭解了 BDD 的由來,接下來,我們就來看看採用 BDD 的方式進行開發,測試會寫成什麼樣子。
今天最流行的 BDD 框架應該是 Cucumber,它的作者就是 RSpec 的作者之一 Aslak Hellesøy。從最開始基於 Ruby 的 BDD 框架發展成今天,Cucumber 已經變成了支持很多不同程式設計語言的 BDD 測試框架,比如常見的 Java、JavaScript、PHP 等等。
下麵是一個 BDD 的示例
Scenario: List todo item
Given todo item "foo" is added
And todo item "bar" is added
When list todo items
Then todo item "foo" should be contained
And todo item "bar" should be contained
從這個例子我們不難看出, BDD 的測試用例有很強的可讀性。即便我們不熟悉技術,單憑這段文字,我們也能看出這個用例想表達的含義。這也就是我們前面說 BDD 測試用例更貼近業務的原因。它希望成為業務人員和技術團隊之間溝通的橋梁,所以,它的表述方式更貼近於業務。
雖然這個表述已經很貼近業務了,但它並不是自然語言描述,而是有一種特定的格式,其實這是一門領域特定語言(Domain Specific Language,簡稱 DSL),稱之為 Gherkin。
不要看到一門新的語言就被嚇退,其實它非常簡單。這裡的核心點就是它的描述格式:“Given…When…Then”。Given 表示一個假設前提,When 表示具體的操作,Then 則對應著這個用例要驗證的結果。
測試一般包含四個階段:準備、執行、斷言和清理。把它對應到這裡,Given 對應著準備,When 對應執行,而 Then 對應斷言。至於清理,這個階段會做一些資源釋放的工作,不過這個工作屬於實現層面的內容,在業務層面上意義不大,所以在以業務描述為主要目標的 BDD 中,這個階段是不存在的。
瞭解了格式,我們再來關註具體的內容。首先,這裡描述的行為都是站在業務的角度進行敘述的。其次,Given、When、Then 都是獨立的,可以自由組合。這也就意味著,一旦基礎框架搭好了,有人就可以使用這些基礎語句來編寫新的測試用例,甚至可以不需要技術人員參與。
從這裡我們不難看出,Gherkin 語言本身有一個很好的目標,與其說它是為了技術人員設計的,不如說它是為了業務人員設計的。
Gherkin 語言這層只提供了業務描述,作為程式員我們很清楚,這層描述並不能直接發揮作用,必須要有一個具體的實現。那具體的實現要放在哪裡呢?這就輪到 膠水層(Glue)發揮作用了,這個將測試用例與實現聯繫起來的膠水層,在 Cucumber 的術語里,稱之為步驟定義(Step Definition),下麵就是一個步驟定義的示例。
public class TodoItemStepDefinitions ... {
private RestTemplate restTemplate;
public TodoItemStepDefinitions() {
...
Given("todo item {string} is added", (String content) ->
addTodoItem(content)
);
...
}
private void addTodoItem(final String content) {
AddTodoItemRequest request = new AddTodoItemRequest(content);
final ResponseEntity<String> entity =
restTemplate.postForEntity("http://localhost:8080/todo-items", request, String.class);
...
}
}
既然步驟定義是 Gherkin 文件與具體實現之間的膠水,所以,理解步驟定義的關鍵就是知道它是如何將二者關聯起來的。在這段代碼中,Given 就是這樣的連接點。對比一下我們就會發現, Given 裡面的參數就是我們在前面 Gherkin 文件中的描述,不同的點是,這裡把其中的一部分變成了參數。由此我們可以知道, 對於同樣一個描述,可以根據用例的差異,採用不同的參數。
如果說 Gherkin 語言部分幾乎在各種 BDD 框架之間是通用的,那步驟定義部分則是框架強相關。這裡我們採用 Cucumber Java 8 的方式進行了步驟定義,也就是採用 Given 方法進行定義,如果你去看其它的資料,也會看到基於 Annotation 的定義,這就是選擇不同依賴程式庫的結果。
到了具體的實現上,程式員就很有底氣了。在這裡我們根據業務動作進行相應的處理。在上面這段代碼中,添加 Todo 項就是向自己編寫的服務發出了一個 POST 請求。
這些東西理解起來都很容易,唯一需要稍微註意一點的是,給 Then 編寫代碼時,因為它是表示斷言的,在這個部分我們一定要寫出斷言,比如像下麵這樣。
Then("todo item {string} should be contained", (String content) -> {
assertThat(Arrays.stream(responses)
.anyMatch(item -> item.getContent().equals(content))).isTrue();
});
實戰中的 BDD
現在我們已經有了對 BDD 的初步瞭解,接下來,我們就來看看在實際的項目中可以怎樣使用 BDD。
前面我們已經知道了,Gherkin 語言是面向業務人員的。不同於寫代碼我們只能用英文,Gherkin 在設計時就考慮到了業務人員的實際需要,所以它的設計本身是本地化的。我們甚至可以用中文編寫測試用例,下麵就是一個登錄的測試用例。
假定 張三是一個註冊用戶,其用戶名密碼是分別是 zhangsan 和 zspassword
當 在用戶名輸入框里輸入 zhangsan,在密碼輸入框里輸入 zspassword
並且 點擊登錄
那麼 張三將登錄成功
這個用例怎麼樣呢?或許你會說,這個用例寫得挺好。如果你這麼想,說明你是站在程式員的視角。我在前面已經說過了,BDD 需要站在業務的角度,而這個例子完全是站在實現的角度。如果登錄方式有所調整,用戶輸完用戶名密碼自動登錄,不需要點擊,那這個用例是不是需要改呢?下麵我換了一種方式描述,你再感受一下。
假定 張三是一個註冊用戶,其用戶名密碼是分別是 zhangsan 和 zspassword
當 用戶以用戶名 zhangsan 和密碼 zspassword 登錄
那麼 張三將登錄成功
這是一個站在業務視角的描述,除非做業務的調整,不用用戶名密碼登錄了,否則這個用例不需要改變。即便實現的具體方式調整了,需要改變的也是具體的步驟定義。所以, 想寫好 BDD 的測試用例,關鍵點在用業務視角描述。
既然 BDD 的用例更多偏向業務視角,所以在真實的項目中使用它時,我們更多偏向於把它當做驗收測試的工具來用。這裡就會有一個我們常常忽略的點:業務測試的模型。很多人的第一直覺是,一個測試要啥模型?
既然 BDD 更多的使用場景是複雜的驗收場景,所以,相應地我們也要為測試場景進行建模。還記得我們講好測試應該具備的屬性嗎?其中一點就是專業性。對於複雜場景而言,想要寫好測試同寫好代碼是一樣的,一個好的模型是不可或缺的。
這方面一個可以作為參考的例子是做 Web 測試常用的一個模型:Page Object。它把對頁面的訪問封裝了起來,即便你在寫的是步驟定義,你也不應該在代碼中直接操作 HTML 元素,而是應該訪問不同的頁面對象。
以前面的登錄為例,我們可能會定義這樣的頁面對象。
public class LoginPage {
public boolean login(String name, String password) {
...
}
}
如此一來,在步驟定義中,你就不必關心具體怎麼定位到輸入框會讓代碼的抽象程度得到提升。當然這隻是一個參考,面對你自己的應用時,你要考慮構建自己的業務測試模型。
BDD 的延伸
最後,我們再來說說 BDD 的一些延伸。從上面的內容我們可以知道,BDD 的用例和普通測試的用例只是在表述方式上有所差異,從結構上看,二者幾乎是完全等價的。所以,只要你想,完全可以採用 BDD 的方式進行從單元測試到系統測試所有類型的測試。
所以我們會看到,在行業里還有一些 BDD 風格的單元測試框架,其中最典型的就是 RSpec。我從 RSpec 的文檔上截取了一段代碼,你可以感受一下。
RSpec.describe Order do
it "sums the prices of its line items" do
order = Order.new
order.add_entry(LineItem.new(:item => Item.new(
:price => Money.new(1.11, :USD)
)))
order.add_entry(LineItem.new(:item => Item.new(
:price => Money.new(2.22, :USD),
:quantity => 2
)))
expect(order.total).to eq(Money.new(5.55, :USD))
end
end
其實,它與前面的 Cucumber 用例還是有很大差異的,因為它屬於單元測試的範疇,所以沒有像 Gherkin 部分那種面向於業務人員的描述。但同時你也能看到,它同傳統的 xUnit 框架有著很大的不同,主要是框架本身會引導你寫出更具描述性的代碼。
BDD 的另外一個延伸方向是對需求進行文檔化的表述。既然 BDD 是在朝著業務方向靠近,爭取讓業務人員能夠很好地理解這些測試用例,那從本質上來說,它就起到了文檔的作用,這個文檔和真實實現是緊密相關的,是一種“活”文檔(Living Document)。活文檔指的是持續更新的文檔,這個概念本身不局限於技術領域。Cucumber 本身有對 活文檔的支持,它可以與 JIRA 去集成,可以直接把 Cucumber 測試用例變成文檔。
既然要寫文檔,那就不局限於是否採用 BDD 這樣的格式,所以,還出現了像 Concordion 這樣的工具,甚至可以讓我們把驗收用例寫成一個完整的參考文檔。最開始它支持用 HTML 的方式寫文檔,現在也支持 用 Markdown 的方式 來編寫文檔。
無論是 BDD 也好,活文檔也罷,它們背後還有一個概念,叫做 實例化需求(Specification by Example,SbE),也就是用實例的方式對需求進行闡述,你可以看到 BDD 和活文檔就是通過這種方式在將需求表現出來。
總之,如果你對這個方向有興趣,前面還是有很多東西可以探索。總的來說,它就是讓技術團隊不再局限於技術本身,更加貼近業務,這和整個行業的發展趨勢是高度吻合的。
總結
今天我們聊了 TDD,也就是測試驅動開發。測試驅動開發已經是行業中的優秀實踐,學習測試驅動開發的第一步是記住測試驅動開發的節奏:紅——綠——重構。
知道了 TDD 的節奏之後,我們還需要理解測試怎樣驅動開發。在 TDD 的過程中,我們要先進行任務分解,把大需求拆成小任務,然後考慮代碼的可測試性,編寫出整潔的代碼,這一切都是在“測試”驅動下產生的。
正是因為視角的轉變,為了編寫可測的代碼,我們甚至要為此調整設計,所以,有人也把 TDD 稱為測試驅動設計。
無論你是否採用 TDD 的實踐,在動手寫代碼之前,從測試的角度進行思考都是非常有價值的一件事,這也是編寫高質量代碼的重要一環。
緊接著我們又談了 BDD,也就是行為驅動開發。這種思想是站在 xUnit 的框架基礎之上,讓測試用例的表達更貼近業務行為。
如果今天的內容你只能記住一件事,那請記住:從測試的視角出發看待代碼, 技術團隊要更加貼近業務。
作者|頂尖架構師棧
本文來自博客園,作者:古道輕風,轉載請註明原文鏈接:https://www.cnblogs.com/88223100/p/Do-you-really-understand-TDD-and-BDD.html