追逐代碼質量: 決心採用 FIT

        JUnit 假定測試的所有方面都是開發人員的地盤,而集成測試框架(FIT)在編寫需求的業務客戶和實現需求的開發人員之間做了協作方面的試驗。這是否意味着 FIT 和 JUnit 是競爭關係呢?絕對不是!代碼質量完美主義者 Andrew Glover 介紹瞭如何把 FIT 和 JUnit 兩者最好的地方結合在一起,實現更好的團隊工作和有效的端到端測試。

  在軟件開發的生命週期中,每個人都對質量負有責任。理想情況下,開發人員在開發週期中,用像 Junit 和 TestNG 這樣的測試工具保證早期質量,而質量保證團隊用功能性系統測試在週期末端跟進,使用像 Selenium 這樣的工具。但是即使擁有優秀的質量保證,有些應用程序在交付的時候仍然被認爲是質量低下的。爲什麼呢?因爲它們並沒有做它們應當做的事。

  在客戶、(編寫應用程序需求的)業務部門和(實現需求的)開發團隊之間的溝通錯誤,通常是摩擦的原因,有時還是開發項目徹底失敗的常見原因。幸運的是,存在一些方法可以幫助需求作者和實現者之間儘早 溝通。

  FIT 化的解決方案

  集成測試框架 (FIT)是一個測試平臺,可以幫助需求編寫人員和把需求變成可執行代碼的人員之間的溝通。使用 FIT,需求被做成表格模型,充當開發人員編寫的測試的數據模型。表格本身充當輸入和測試的預期輸出。

  圖 1 顯示了用 FIT 創建的結構化模型。第一行是測試名稱,下一行的三列是與輸入(value1value2)和預期結果(trend())有關的標題。

圖 1. 用 FIT 創建的結構化模型

  好消息是,對於編程沒有經驗的人也能編寫這個表格。FIT 的設計目的就是讓消費者或業務團隊在開發週期中,儘早與實現他們想法的開發人員協作。創建應用程序需求的簡單表格式模型,可以讓每個人清楚地看出代碼和需求是否是一致的。

  清單 1 是與圖 1 的數據模型對應的 FIT 代碼。不要太多地擔心細節 —— 只要注意代碼有多麼簡單,而且代碼中沒有包含驗證邏輯(例如,斷言等)。可能還會注意到一些與表 1 中的內容匹配的變量和方法名稱;關於這方面的內容後面介紹。


  清單 1. 根據 FIT 模型編寫的代碼



  清單 1 中的代碼由研究上面表格並插入適當代碼的開發人員編寫。最後,把所有東西合在一起,FIT 框架讀取表 1 的數據,調用對應的代碼,並確定結果。



  FIT 和 JUnit

  FIT 的優美之處在於,它讓組織的消費者或業務端能夠儘早參與測試過程(例如,在開發期間)。JUnit 的力量在於編碼過程中的單元測試,而 FIT 是更高層次的測試工具,用來判斷規劃的需求實現的正確性。

  例如,雖然 JUnit 擅長驗證兩個 Money 對象的合計與它們的兩個值的合計相同,但 FIT 可以驗證總的訂單價格是其中商品的價格減去任何相關折扣之後的合計。區別雖然細微,但的確重大!在 JUnit 示例中,要處理具體的對象(或者需求的實現),但是使用 FIT 時要處理的是高級的業務過程。 

  這很有意義,因爲編寫需求的人通常不太考慮 Money 對象 —— 實際上,他們可能根本不知道這類東西的存在!但是,他們確實要考慮,當商品被添加到訂單時,總的訂單價格應當是商品的價格減去所有折扣。

  FIT 和 JUnit 之間絕不是競爭關係,它們是保證代碼質量的好搭檔,正如在後面的 案例研究 中將要看到的。



  測試用的 FIT 表格

  表格是 FIT 的核心。有幾種不同類型的表格(用於不同的業務場景),FIT 用戶可以用不同的格式編寫表格。用 HTML 編寫表格甚至用 Microsoft Excel 編寫都是可以的,如圖 2 所示:


  圖 2. 用 Microsoft Excel 編寫的表格

  也有可能用 Microsoft Word 這樣的工具編寫表格,然後用 HTML 格式保存,如圖 3 所示:


  圖 3. 用 Microsoft Word 編寫的表格

  開發人員編寫的用來執行表格數據的代碼叫作裝備(fixture)。要創建一個裝備類型,必須擴展對應的 FIT 裝備,它映射到對應的表。如前所述,不同類型的表映射到不同的業務場景。


  用裝備進行裝配

  最簡單的表和裝備組合,也是 FIT 中最常用的,是一個簡單的列表格,其中的列映射到預期過程的輸入和輸出。對應的裝備類型是 ColumnFixture

  如果再次查看 清單 1,將注意到 TrendIndicator 類擴展了 ColumnFixture,而且也與圖 3 對應。請注意在圖 3 中,第一行的名稱匹配完全限定名稱(test.com.acme.fit.impl.TrendIndicator)。下一行有三列。頭兩個單元格的值匹配 TrendIndicator 類的 public 實例成員(value1value2),最後一個單元格的值只匹配 TrendIndicator 中的方法(trend)。

  現在來看清單 1 中的 trend 方法。它返回一個 String 值。可以猜測得到,對於表中每個剩下的行,FIT 都會替換值並比較結果。在這個示例中,有三個 “數據” 行,所以 FIT 運行 TrendIndicator 裝備三次。第一次,value1 被設置成 84.0,value2 設置成 71.2。然後 FIT 調用 trend 方法,並把從方法得到的值與表中的值比較,應當是 “decreasing”。

  通過這種方式,FIT 用裝備代碼測試 Trender 類,每次 FIT 執行 trend 方法時,都執行類的 determineTrend 方法。當代碼測試完成時,FIT 生成如圖 4 所示的報告:


  圖 4. FIT 報告 trend 測試的結果

 

trend 列單元格的綠色表明測試通過(例如,FIT 設置 value1 爲 84.0,value2 爲 71.2,調用 trend 得到返回值 “decreasing”)。


  查看 FIT 運行

  可以通過命令行,用 Ant 任務並通過 Maven 調用 FIT,從而簡單地把 FIT 測試插入構建過程。因爲自動進行 FIT 測試,就像 JUnit 測試一樣,所以也可以定期運行它們,例如在持續集成系統中。

  最簡單的命令行運行器,如清單 2 所示,是 FIT 的 FolderRunner,它接受兩個參數 —— 一個是 FIT 表格的位置,一個是結果寫入的位置。不要忘記配置類路徑!


  清單 2. FIT 的命令行



  FIT 通過插件,還可以很好地與 Maven 一起工作,如清單 3 所示。只要下載插件,運行 fit:fit 命令,就 OK 了!


  清單 3. Maven 得到 FIT




  試用 FIT:案例研究

  現在已經瞭解了 FIT 的基礎知識,我們來做一個練習。如果還沒有 下載 FIT,現在是下載它的時候了!如前所述,這個案例研究顯示出可以容易地把 FIT 和 JUnit 測試組合在一起,形成多層質量保證。

  假設現在要爲一個釀酒廠構建一個訂單處理系統。釀酒廠銷售各種類型的酒類,但是它們可以組織成兩大類:季節性的和全年性的。因爲釀酒廠以批發方式運作,所以酒類銷售都是按桶銷售的。對於零售商來說,購買多桶酒的好處就是折扣,而具體的折扣根據購買的桶數和酒是季節性還是全年性的而不同。

  麻煩的地方在於管理這些需求。例如,如果零售店購買了 50 桶季節性酒,就沒有折扣;但是如果這 50 桶不是 季節性的,那麼就有 12% 的折扣。如果零售店購買100 桶季節性酒,那就有折扣,但是隻有 5%。100 桶更陳的非季節性酒的折扣達到 17%。購買量達到 200 時,也有類似的規矩。

  對於開發人員,像這樣的需求集可能讓人摸不着頭腦。但是請看,我們的啤酒-釀造行業分析師用 FIT 表可以很容易地描述出這個需求,如圖 5 所示:


  圖 5. 我的業務需求非常清晰!

  表格語義

  這個表格從業務的角度來說很有意義,它確實很好地規劃出需求。但是作爲開發人員,還需要對錶格的語言瞭解更多一些,以便從表格得到值。首先,也是最重要的,表格中的初始行說明表格的名稱,它恰好與一個匹配的類對應(org.acme.store.discount.DiscountStructureFIT)。命名要求表格作者和開發人員之間的一些協調。至少,需要指定完全限定的表格名稱(也就是說,必須包含包名,因爲 FIT 要動態地裝入對應的類)。

  請注意表格的名稱以 FIT 結束。第一個傾向可能是用 Test 結束它,但要是這麼做,那麼在自動環境中運行 FIT 測試和 JUnit 測試時,會與 JUnit 產生些衝突,JUnit 的類通常通過命名模式查找,所以最好避免用 Test 開始或結束 FIT 表格名稱。

  下一行包含五列。每個單元格中的字符串都特意用斜體格式,這是 FIT 的要求。前面學過,單元格名稱與裝備的實例成員和方法匹配。爲了更簡潔,FIT 假設任何值以括號結束的單元格是方法,任何值不以括號結束的單元格是實例成員。

  特殊智能

  FIT 在處理單元格的值,進行與對應裝備類的匹配時,採用智能解析。如 圖 5 所示,第二行單元格中的值是用普通的英文編寫的,例如 “number of cases”。FIT 試圖把這樣的字符串按照首字母大寫方式連接起來;例如,“number of cases” 變成 “numberOfCases”,然後 FIT 試圖找到對應的裝備類。這個原則也適用於方法 —— 如圖 5 所示,“discount price()” 變成了 “discountPrice()”。

  FIT 還會智能地猜測單元格中值的具體類型。例如,在 圖 5 餘下的八行中,每一列都有對應的類型,或者可以由 FIT 準確地猜出,或者要求一些定製編程。在這個示例中,圖 5 有三種不同類型。與 “number of cases” 關聯的列匹配到 int,而與 “is seasonal” 列關聯的值則匹配成 boolean

  剩下的三列,“list price per case”、“discount price()” 和 “discount amount()” 顯然代表當前值。這幾列要求定製類型,我將把它叫作 Money。有了它之後,應用程序就要求一個代表錢的對象,所以在我的 FIT 裝備中遵守少量語義就可以利用上這個對象!

  FIT 語義總結

  表 1 總結了命名單元格和對應的裝備實例變量之間的關係:


  表 1. 單元格到裝備的關係:實例變量

  單元格值 對應的裝備實例變量 類型
  list price per case listPricePerCase Money
  number of cases numberOfCases int
  is seasonal isSeasonal boolean

  表 2 總結了 FIT 命名單元格和對應的裝備方法之間的關係:


  表 2. 單元格到裝備的關係:方法

  表格單元格的值 對應的裝備方法 返回類型
  discount price() discountPrice Money
  discount amount() discountAmount Money



  該構建了!

  要爲釀酒廠構建的訂單處理系統有三個主要對象:一個 PricingEngine 處理包含折扣的業務規則,一個 WholeSaleOrder 代表訂單,一個 Money 類型代表錢。

  Money 類

  第一個要編寫的類是 Money 類,它有進行加、乘和減的方法。可以用 JUnit 測試新創建的類,如清單 14 所示:

  
  清單 4. JUnit 的 MoneyTest 類



  WholeSaleOrder 類

  然後,定義 WholeSaleOrder 類型。這個新對象是應用程序的核心:如果 WholeSaleOrder 類型配置了桶數、每桶價格和產品類型(季節性或全年性),就可以把它交給 PricingEngine,由後者確定對應的折扣並相應地在 WholeSaleOrder 實例中配置它。

  WholesaleOrder 類的定義如清單 5 所示:


  清單 5. WholesaleOrder 類



  從清單 5 中可以看到,一旦在 WholeSaleOrder 實例中設置了折扣,就可以通過分別調用 getCalculatedPricegetDiscountedDifference 方法得到折扣價格和節省的錢。

  更好地測試這些方法(用 JUnit)!

  定義了 MoneyWholesaleOrder 類之後,還要編寫 JUnit 測試來驗證 getCalculatedPricegetDiscountedDifference 方法的功能。測試如清單 6 所示:


  清單 6. JUnit 的 WholesaleOrderTest 類



  PricingEngine 類

  PricingEngine 類利用業務規則引擎,在這個示例中,是 Drools。PricingEngine 極爲簡單,只有一個 public 方法:applyDiscount。只要傳遞進一個 WholeSaleOrder 實例,引擎就會要求 Drools 應用折扣,如清單 7 所示:


  清單 7. PricingEngine 類


 

Drools 的規則

  必須在特定於 Drools 的 XML 文件中定義計算折扣的業務規則。例如,清單 8 中的代碼段就是一個規則:如果桶數大於 9,小於 50,不是季節性產品,則訂單有 5% 的折扣。

  清單 8. BusinessRules.drl 文件的示例規則






  標記團隊測試

  有了 PricingEngine 並定義了應用程序規則之後,可能渴望驗證所有東西都工作正確。現在問題就變成,用 JUnit 還是 FIT?爲什麼不兩者都用呢?通過 JUnit 測試所有組合是可能的,但是要進行許多編碼。最好是用 JUnit 測試少數幾個值,迅速地驗證代碼在工作,然後依靠 FIT 的力量運行想要的組合。請看看當我這麼嘗試時發生了什麼,從清單 9 開始:


  清單 9. JUnit 迅速地驗證了代碼在工作



  還沒用 FIT?那就用 FIT!

  在 圖 5 的 FIT 表格中有八行數據值。可能已經在 清單 7 中編寫了前兩行的 JUnit 代碼,但是真的想編寫整個測試嗎?編寫全部八行的測試或者在客戶添加新規則時再添加新的測試,需要巨大的耐心。好消息就是,現在有了更容易的方法。不過,不是忽略測試 —— 而是用 FIT! 

  FIT 對於測試業務規則或涉及組合值的內容來說非常漂亮。更好的是,其他人可以完成在表格中定義這些組合的工作。但是,在爲表格創建 FIT 裝備之前,需要給 Money 類添加一個特殊方法。因爲需要在 FIT 表格中代表當前貨幣值(例如,像 $100.00 這樣的值),需要一種方法讓 FIT 能夠認識 Money 的實例。做這件事需要兩步:首先,必須把 static parse 方法添加到定製數據類型,如清單 10 所示:


清單 10. 添加 parse 方法到 Money 類



  Money 類的 parse 方法接受一個 String 值(例如,FIT 從表格中取出的值)並返回配置正確的 Money 實例。在這個示例中,$ 字符被刪除,剩下的 String 被轉變成 double,這與 Money 中現有的構造函數匹配。

  不要忘記向 MoneyTest 類添加一些測試來來驗證新添加的 parse 方法按預期要求工作。兩個新測試如清單 11 所示:

 
  清單 11. 測試 Money 類的 parse 方法



  編寫 FIT 裝備

  現在可以編寫第一個 FIT 裝備了。實例成員和方法已經在表 1 和表 2 中列出,所以只需要把事情串在一起,添加一兩個方法來處理定製類型:Money。爲了在裝備中處理特定類型,還需要添加另一個 parse 方法。這個方法的簽名與前一個略有不同:這個方法是個對 Fixture 類進行覆蓋的實例方法,這個類是 ColumnFixture 的雙親。

  請注意在清單 12 中,DiscountStructureFITparse 方法如何比較 class 類型。如果存在匹配,就調用 Money 的定製 parse 方法;否則,就調用父類(Fixture)的 parse 版本。

  清單 12 中剩下的代碼是很簡單的。對於圖 5 所示的 FIT 表格中的每個數據行,都設置值並調用方法,然後 FIT 驗證結果!例如,在 FIT 測試的第一次運行中,DiscountStructureFITlistPricePerCase 被設爲 $10.00,numberOfCases 設爲 10,isSeasonal 爲 true。然後執行 DiscountStructureFITdiscountPrice,返回的值與 $100.00 比較,然後執行 discountAmount,返回的值與 $0.00 比較。


  清單 12. 用 FIT 進行的折扣測試



  現在,比較 清單 9 的 JUnit 測試用例和清單 12。是不是清單 12 更有效率?當然可以 用 JUnit 編寫所有必需的測試,但是 FIT 可以讓工作容易得多!如果感覺到滿意(應當是滿意的!),可以運行構建,調用 FIT 運行器生成如圖 6 所示的結果:


  圖 6. 這些結果真的很 FIT !



  結束語

  FIT 可以幫助企業避免客戶和開發人員之間的溝通不暢、誤解和誤讀。把編寫需求的人儘早 帶入測試過程,是在問題成爲開發惡夢的根源之前發現並修補它們的明顯途徑。而且,FIT 與現有的技術(比如 JUnit)完全兼容。實際上,正如本文所示,JUnit 和 FIT 互相補充。請把今年變成您追逐代碼質量 的重要紀年 —— 由於決心採用 FIT!

))); }

  Money 類的 parse 方法接受一個 String 值(例如,FIT 從表格中取出的值)並返回配置正確的 Money 實例。在這個示例中,$ 字符被刪除,剩下的 String 被轉變成 double,這與 Money 中現有的構造函數匹配。

  不要忘記向 MoneyTest 類添加一些測試來來驗證新添加的 parse 方法按預期要求工作。兩個新測試如清單 11 所示:

 
  清單 11. 測試 Money 類的 parse 方法

___FCKpd___10

  編寫 FIT 裝備

  現在可以編寫第一個 FIT 裝備了。實例成員和方法已經在表 1 和表 2 中列出,所以只需要把事情串在一起,添加一兩個方法來處理定製類型:Money。爲了在裝備中處理特定類型,還需要添加另一個 parse 方法。這個方法的簽名與前一個略有不同:這個方法是個對 Fixture 類進行覆蓋的實例方法,這個類是 ColumnFixture 的雙親。

  請注意在清單 12 中,DiscountStructureFITparse 方法如何比較 class 類型。如果存在匹配,就調用 Money 的定製 parse 方法;否則,就調用父類(Fixture)的 parse 版本。

  清單 12 中剩下的代碼是很簡單的。對於圖 5 所示的 FIT 表格中的每個數據行,都設置值並調用方法,然後 FIT 驗證結果!例如,在 FIT 測試的第一次運行中,DiscountStructureFITlistPricePerCase 被設爲 $10.00,numberOfCases 設爲 10,isSeasonal 爲 true。然後執行 DiscountStructureFITdiscountPrice,返回的值與 $100.00 比較,然後執行 discountAmount,返回的值與 $0.00 比較。


  清單 12. 用 FIT 進行的折扣測試

___FCKpd___11

  現在,比較 清單 9 的 JUnit 測試用例和清單 12。是不是清單 12 更有效率?當然可以 用 JUnit 編寫所有必需的測試,但是 FIT 可以讓工作容易得多!如果感覺到滿意(應當是滿意的!),可以運行構建,調用 FIT 運行器生成如圖 6 所示的結果:


  圖 6. 這些結果真的很 FIT !



  結束語

  FIT 可以幫助企業避免客戶和開發人員之間的溝通不暢、誤解和誤讀。把編寫需求的人儘早 帶入測試過程,是在問題成爲開發惡夢的根源之前發現並修補它們的明顯途徑。而且,FIT 與現有的技術(比如 JUnit)完全兼容。實際上,正如本文所示,JUnit 和 FIT 互相補充。請把今年變成您追逐代碼質量 的重要紀年 —— 由於決心採用 FIT!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章