閱讀119 返回首頁    go 阿裏雲 go 技術社區[雲棲]


《Cucumber:行為驅動開發指南》——6.3 照管好你的測試

本節書摘來自異步社區《Cucumber:行為驅動開發指南》一書中的第6章,第6.3節,作者:【英】Matt Wynne , 【挪】Aslak Hellesy著,更多章節內容可以訪問雲棲社區“異步社區”公眾號查看

6.3 照管好你的測試

自動化特性的好處在於你可以把它們作為活文檔來長期信賴,因為你會將每一個場景都用於檢查產品代碼,以確保它們仍然有效。對於同代碼打交道的程序員來說,這還有另一件好處:在他們開發係統的時候,那些測試可以充當安全網,對任何破壞已有行為的錯誤都給出警告。

因此,你的特性可以充當一種反饋機製,對整個團隊來說提供關於係統行為的反饋,對程序員來說還能提供是否破壞已有行為的反饋。想讓這些反饋循環帶來好處,測試需要執行迅速,還需要可靠。我們首先來看看影響測試可靠性的問題。
6.3.1 滲露的場景
Cucumber的場景從根本上講就是狀態轉換測試:你將係統置於給定的(Given)狀態A,執行動作X(When),然後(Then)驗證它遷移到了期望的狀態B。因此,每個場景在開始運行之前都需要係統處於某種確定的狀態,而每個場景結束時也要把係統置於一種髒的新狀態。

如果在兩個測試之間沒有重置係統的狀態,我們就說狀態在測試之間發生了滲露(leak)。這是導致測試脆弱的主要原因之一。

如果一個場景需依賴之前另一個場景留下的狀態才能通過,就說明你在兩個場景之間製造了依賴。如果有一連串的場景像火車車廂一樣彼此依賴,那麼離火車事故就為時不遠了。

如果第一個場景,也就是正好按照下一個場景的需要將係統狀態準備好的那個場景,發生了變化,那麼突然之間後麵那個就要失敗了。即使沒有改動前麵一個,可如果你隻想單獨運行第二個場景,那又會怎樣呢?沒有了前一個場景滲露下來的狀態,它還是會失敗。

這種情況的反麵,即獨立的場景,可以確保場景將係統置於幹淨的狀態,然後再在上麵添加自己的數據。這使場景能夠自給自足,而不是跟其他測試留下的數據或共享的固件數據耦合到一起。投入些時間精力來構造一個良好可靠的測試數據構造器的庫,可以更容易達到獨立場景的目標。

獨立的場景對於成功自動化測試的重要性怎麼強調都不為過。除了獨立設置自身數據的場景所帶來的附加可靠性之外,它們讀起來、調試起來都更加清楚。當你僅靠閱讀場景就能準確地看清它所使用的數據,而不需要在固件數據腳本中甚至更麻煩地直接在數據庫中四處查閱時,理解或診斷一次失敗就容易多了。

測試數據構造器

如果你使用Ruby,那肯定熟悉FactoryGirla這個gem。FactoryGirl是測試數據構造器(Test Data Builder)b這一模式的出色實現。如果你還不太了解,以下內容簡單概括了它的好處。

假如你正在測試一個工資單係統,作為某個場景的一部分,你需要創建一條PayCheck記錄。按照你的領域模型結構,PayCheck需要一個Employee,而Employee需要一個Address。每種結構還有其他一些必要的字段。你不需要在步驟定義代碼中分別創建所有這些對象,也不必維護一大堆固件數據,隻需要這樣做:

Given /^I have been paid$/ do
 Factory :pay_check
end

基於你的數據模型,隻要用模型的結構配置好FactoryGirl(配置的細節參閱FactoryGirl文檔),然後你隻需要向它要一個PayCheck,FactoryGirl便會創建好PayCheck記錄以及它所依賴的所有對象,並用適當的默認值設置好那些必要的字段。如果你喜歡讓某個字段擁有特定的值,可以讓FactoryGirl把默認值覆蓋掉:

Given /^I have been paid 50 dollars$/ do
 Factory :pay_check, :amount => 50
end

當數據的創建可以如此方便時,你就不再需要走到哪裏都拖著一大包固件數據。當然,創建這樣的構造器需要一小筆前期投資,但很快就會取得回報,那就是可靠的、可讀的場景和步驟定義代碼。如果你的團隊不使用Ruby也不要緊,隻需要很少的額外工作,你仍然可以讓ActiveRecord和FactoryGirl指向其數據庫。如果不行,你還可以針對自己使用的語言找找其他類似工具。

6.3.2 競爭條件和打瞌睡的步驟
給一個足夠複雜的係統編寫端到端集成測試時,你終歸會遇到競爭條件和沉睡的步驟的問題。當係統的兩個或多個部分並發執行,但執行是否成功需仰仗其中某一部分首先結束,這時就會發生競爭條件。就Cucumber測試來說,你的When步驟可能導致係統啟動一些後台運行的工作,比如產生一份PDF或者更新一組搜索索引。如果後台任務碰巧在Cucumber

Matt說:fixture這一術語有不同的含義

在自動化測試領域,詞語fixture至少有三種意思,這有時會引起混淆。在這一章我們使用術語固件數據(fixture data)表示用來給場景或測試用例設置上下文的數據。這是該術語在各種xUnit測試工具a以及Ruby on Rails框架中使用的最常見的意思。

有一種古老的傳統(源於開創測試固件這一術語的硬件世界)是將測試係統與被測係統之間的連接稱為fixture。這是“粘合代碼”的角色,本書中我們稱之為自動化代碼(automation code)。FIT測試框架b用的就是該術語的這種含義。

一些單元測試工具(如NUnit)把水攪得更混了,它們把測試用例類本身稱為fixture。關於通用語言,就說到這裏吧!
執行Then步驟之前結束,場景就會通過。而如果Cucumber贏得了競爭,Then步驟在後台任務執行之前便執行了,那場景則會失敗。

如果競爭雙方勢均力敵,你會遇上一個閃爍的場景,場景間歇地成功或失敗。而如果一方勝算較多,競爭條件就會長期存在而不為人所知,直到某一天係統中某處新的變化平均了雙方的籌碼,場景便開始隨機地失敗了。

針對這種問題,一種粗糙的解決方法是在場景中引入固定時長的停頓或睡眠,從而為係統騰出時間來完成後台任務的處理。診斷競爭條件時這絕對是一種有用的短期技巧,然而,你還是應當抵住誘惑,一旦弄清了問題的原因就不要在測試中留下睡眠。引入打瞌睡的步驟不會解決場景閃爍的問題,隻會讓它發生的概率更低。同時,引入睡眠也會為測試的整體運行時間再增加額外的幾秒,用一個問題換一個問題罷了。

如果一定要做選擇,我們寧可要緩慢但可靠的測試也不要更快但不可靠的測試,不過我們沒必要做這樣的折中。當測試人員和程序員結對將場景自動化的時候,他們可以基於對係統工作原理的理解精心地編寫測試。也就是說,他們可以利用係統中隱含的線索讓測試知道何時能夠安全前進,因而測試可以盡快地前進,而不必使用粗陋的定長睡眠。關於處理異步代碼的示例及更多細節,可以參閱第9章。

6.3.3 共享的環境
在那些從手工驗收測試體係轉向使用自動化驗收測試的團隊中,共享的環境是我們經常發現的一個問題。按照傳統方法,團隊中的手工測試人員會使用一種稱為係統測試環境的特殊環境,其中部署係統最近的構建版本。測試人員將在這一環境中運行自己的手工測試,並將bug報告給開發團隊。如果有多名測試人員需要在同樣的環境上運行測試,他們就需要彼此溝通,確保不會影響對方。

團隊開始將測試自動化的時候,在一套新的環境中安裝係統哪怕稍微有點麻煩,大家都很可能本著最小阻力的原則,把各自的測試腳本全弄到這套已有的係統測試環境中來。現在測試環境不僅在團隊成員之間共享,也在測試腳本之間共享了。假設某一名開發人員收到了一個bug報告,想親自重現一下,可他並未意識到自動化測試此時也在運行。作為bug重現步驟的一部分,開發人員無心地刪除了自動化測試所依賴的一條數據庫記錄,自動化測試自然失敗了。這種情形是導致閃爍的場景的一種典型情況。

對單一環境的共享使用還會對數據庫這類炙手可熱的資源造成沉重且不穩定的負荷,從而促成不可靠的測試。當共享的數據庫超負荷運轉時,正常情況下的可靠測試也會因超時而失敗。

要解決這一問題,在新的環境中啟動係統必須做到易如反掌。

6.3.4 被隔離的測試人員
測試人員在軟件團隊中常被視為二等公民。我們將在第 8 章中解釋,開發一組健康的Cucumber特性套件不隻需要測試技巧,也需要編程技巧。如果測試人員被晾在一邊獨自構建Cucumber測試,他們可能不具備讓步驟定義和支持代碼組織良好的軟件工程技巧。不知不覺中,測試會變得一團雜亂,且脆弱得沒人敢去改動。

為克服這一問題,編寫步驟定義和支持代碼時要鼓勵測試人員和程序員協同工作。程序員可以向測試人員展示如何組織代碼,使之結構清晰,如何提取可重用組件和庫,以供其他團隊使用。有些庫,比如Capybara(參閱第15章),就是在程序員從團隊的步驟定義中提取可重用代碼時這樣產生出來的。通過與測試人員結對,程序員對如何讓代碼可測試也會產生更深的理解。

一個團隊如果把Cucumber用到好處,測試人員應該能夠將運行基礎檢查的工作托付給Cucumber。這樣他們自己就可以解放出來,去做更有趣、更有創造性的探索性測試(exploratory testing)工作,就像Agile Testing: A Practical Guide for Testers and Agile Teams [CG08]一書中闡述的那樣。

6.3.5 固件數據
手工測試一套係統的時候,為它提供真實數據非常有用,這樣你便如同在真實應用中使用係統。團隊從手工測試轉向自動測試的時候,你常常很想隻移植產品數據的一個子集,從而讓自動測試可以快速跟一個正常運行的係統交互。

一鍵式係統搭建

要避免因使用共享環境導致的閃爍的場景,團隊需要一份腳本,做到隻需按鍵一點就可以從零開始創建一份新的係統實例。

如果係統使用數據庫,腳本產生的數據庫應包含最新的模式(schema),以及所有的存儲過程、視圖、函數等。它應當僅包含係統正常運轉所需的最少基礎數據,比如配置數據。任何其他數據都應該等各個獨立的場景自己去創建。

如果係統中有消息隊列,或者memcache守護進程,搭建腳本也要啟動它們,且使用你期望運行係統中應有的最低配置。

即使團隊不在產品代碼中使用Ruby,他們也可以試著用Ruby的ActiveRecorda gem來管理數據庫模式和遷移腳本。ActiveRecord能讓這類日常雜務變得輕而易舉。
另一種方法,讓每個測試構建自己的數據,看上去難度太大。在遺留係統中——特別是係統設計經不斷發展而來的那種,創建當前測試所需的單個對象都意味著為它創建所依賴的全部對象構成的一整棵大樹,你會覺得最方便的辦法就是在固件數據(fixture data)中把它一次性創建好,然後與其他測試共享這棵大樹。

這一方法有幾個嚴重的問題。一組固件數據,即使開始時相對精簡,時間長了體積也會隻增不減。隨著越來越多的場景開始依賴這些數據,且每個場景都要對它們做一點特別的處理,固件數據的體積會越來越大,複雜度也會越來越高。當你為了某個場景對數據做點改動,結果卻使其他場景失敗時,你將開始感受到脆弱的特性的痛苦。有這麼多不同的場景依賴於固件數據,你會傾向於煳上更多的數據,因為這比修改已有的數據更加安全。某個場景失敗時,你很難清楚係統中的哪一團數據可能跟失敗相關,診斷起來也更加困難。

如果固件數據的集合體積龐大,在兩個測試之間準備這些數據就會變慢。這會給你帶來壓力,誘使你編寫滲露的場景,從而係統的狀態不會在兩個場景之間重置,因為這樣速度更快。但這樣做的後果我們前麵已經解釋過了。

我們認為固件數據是一種反模式(antipattern)。我們更傾向於使用之前介紹過的測試數據構造器,比如FactoryGirl1,那樣可以讓測試本身從內部創建相關數據,而不是讓這些數據淹沒在一大團混亂的固件數據集合中。
每日構建

當你碰到由大量場景所導致的緩慢的特性時,有必要考慮將構建拆成兩部分。用標簽來標記那些每次提交代碼時都應運行的場景,其他的都降為隻在每夜構建時運行。

這一模式的使用依賴於團隊願意承擔風險的程度,以及犯錯的趨向。降為每夜構建時運行的場景是那些極少失敗甚至不失敗的場景。它們針對的是那些幾個月都不發生改變的功能,覆蓋的是那些目前沒有開發動作的穩定代碼。它們是不得已時你情願全部刪除的場景。

為適當的場景打上針對簽入構建的標簽會帶來額外的維護成本。一段時間後,這些場景中有一部分將趨於穩定,這時應該把它們降低到每夜構建中,然後用新的場景替換它們。

雖然每夜構建可以作為擺脫困境的好方法,通常來說長期的正確方案還是會打破你的大泥球。
6.3.6 大量場景
看起來似乎是陳述常識,可是擁有大量的場景是最容易導致特性整體運行緩慢的原因。我們並不是建議你放棄BDD並回到“牛仔式編程”(cowboy coding)當中,但我們真心建議你把緩慢的特性看做一個危險信號。除了需要很長時間等待反饋,擁有大量的測試還有其他的不利影響。數量巨大的特性很難組織得井井有條,閱讀者也會覺得在其中前後翻閱極不方便。底層步驟定義和支持代碼的維護同樣變得更加困難。

我們發現,使用單一龐大構建的團隊也更傾向於使用一種最適合用“大泥球”來描述的架構。由於係統的全部行為都實現在一個地方,所有的測試也隻能放在一個地方,且隻能作為一個巨塊一起運行。在生存時間較長的Ruby on Rails應用中,這是一種典型的症狀,這樣的應用通常都經過長期持續的發展,各子係統間卻沒有清楚的接口。

我們會在下一節討論更多關於如何處理“大泥球”的內容。正視這一問題,在端倪初現時一次性解決它至關重要,然而它並不是你在一夜之間就能解決的一個問題。

與此同時,你可以使用子文件夾和標簽來保持特性的合理組織(參見第5章)。標簽對此好處尤多,因為你可以用標簽來拆分測試。你可以並發運行拆分後的不同測試集合,甚至把某些部分降為隻在每夜構建(參見6.3.5節)時運行。

還有一種方法值得考慮,對於你在Cucumber場景中描述的行為,是否有一些可以下移一層,使用快速的單元測試來表達?熱情擁抱Cucumber的團隊有時會忘了還有單元測試這回事,而過分地依賴緩慢的集成測試來獲取反饋。試著將Cucumber場景想象成向業務人員傳達代碼一般行為的粗略描述,同時仍然要靠快速的單元測試來獲得盡可能高的覆蓋率。實現Cucumber場景時讓測試人員和程序員結對工作,從而協助達成這一目標。係統的某種行為需要用緩慢的端到端Cucumber場景來實現呢,還是應通過快速的單元測試來驅動呢?測試人員和程序員的結對可以對此做出恰當的決策。

6.3.7 大泥球
大泥球(Big Ball of Mud)2是針對某一類軟件設計的一個具有諷刺意味的詞語,在這種軟件設計中,你實際上看不到任何人真正為軟件設計做點什麼。換句話說,整個軟件結構就是一大團亂麻。

我們已經解釋了大泥球會在Cucumber測試中引發哪些問題——緩慢的特性、固件數據和共享的環境,這些都是它可能導致的麻煩。要警惕這些信號並敢於對係統設計做出改變,從而使係統測試起來更加容易。

我們建議你把Alistair Cockburn的端口和適配器(ports and adapter)3架構作為設計係統的方法,從而使係統可測試。Michael Feathers的Working Effectively with Legacy Code [Fea04]一書提供了很多實用的例子,教你如何把原先設計時沒有考慮測試的大型係統分解開來。

跟團隊定期召開討論係統架構的會議:大家喜歡哪些方麵,不喜歡哪些方麵,又有哪些方麵是大家願意接受的。大家總是容易被雄心勃勃的想法弄得情緒高漲,可回到座位上不久那些想法就很快消失在稀薄的空氣中,因此要確保討論結束時盡量得出現實可行的步驟,從而沿著正確的方向推動大家的工作。

以上基本涵蓋了你在團隊中采用Cucumber時可能遭遇的最常見的問題。然而,理解這些問題是一回事,抽出時間來處理它們則完全是另一回事了。下一節我們就來討論一種幫你找到合適時間的重要技巧。

最後更新:2017-06-05 13:31:27

  上一篇:go  《Cucumber:行為驅動開發指南》——6.4 停掉生產線和缺陷預防
  下一篇:go  《Cucumber:行為驅動開發指南》——6.2 同心協力