熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java高級技術 >> 正文

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

2022-06-13   來源: Java高級技術 

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

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

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

  FIT 化的解決方案

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

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

   用 FIT 創建的結構化模型

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

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

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

   package acmefitimpl; import comacmesedlptrendTrender; import fitColumnFixture; public class TrendIndicator extends ColumnFixture { public double value; public double value; public String trend(){ return TrenderdetermineTrend(value value)getName(); } }


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

  
  FIT 和 JUnit

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

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

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

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

  

  測試用的 FIT 表格

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

   用 Microsoft Excel 編寫的表格

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

   用 Microsoft Word 編寫的表格

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

  用裝備進行裝配

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

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

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

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

   FIT 報告 trend 測試的結果

  trend 列單元格的綠色表明測試通過(例如FIT 設置 valuevalue調用 trend 得到返回值 decreasing

  查看 FIT 運行

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

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

  清單 FIT 的命令行

   %>java fitrunnerFolderRunner /test/fit /target/


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

  清單 Maven 得到 FIT

   C:\dev\proj\edoa>maven fit:fit __ __ | \/ |__ _Apache__ ___ | |\/| / _` \ V / _) \ ~ intelligent projects ~ |_| |_\___|\_/\___|_||_| v build:start: java:preparefilesystem: java:compile: [echo] Compiling to C:\dev\proj\edoa/target/classes java:jarresources: test:preparefilesystem: test:testresources: test:compile: fit:fit: [java] right wrong ignored exceptions BUILD SUCCESSFUL Total time: seconds Finished at: Thu Feb :: EST



  試用 FIT案例研究

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

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

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

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

   我的業務需求非常清晰!

  表格語義

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

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

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

  特殊智能

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

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

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

  FIT 語義總結

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

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

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

   單元格到裝備的關系方法

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

  該構建了!

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

  Money 類

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

  清單 JUnit 的 MoneyTest 類

   package orgacmestore; import junitframeworkTestCase; public class MoneyTest extends TestCase { public void testToString() throws Exception{ Money money = new Money(); Money total = moneympy(); assertEquals($ totaltoString()); } public void testEquals() throws Exception{ Money money = Moneyparse($); Money control = new Money(); assertEquals(control money); } public void testMultiply() throws Exception{ Money money = new Money(); Money total = moneympy(); Money discountAmount = totalmpy(); assertEquals($ discountAmounttoString()); } public void testSubtract() throws Exception{ Money money = new Money(); Money total = moneympy(); Money discountAmount = totalmpy(); Money discountedPrice = totalsub(discountAmount); assertEquals($ discountedPricetoString()); } }


  WholeSaleOrder 類

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

    WholesaleOrder 類的定義如清單 所示

  清單 WholesaleOrder 類

   package orgacmestorediscountengine; import orgacmestoreMoney; public class WholesaleOrder { private int numberOfCases; private ProductType productType; private Money pricePerCase; private double discount; public double getDiscount() { return discount; } public void setDiscount(double discount) { thisdiscount = discount; } public Money getCalculatedPrice() { Money totalPrice = thispricePerCasempy(thisnumberOfCases); Money tmpPrice = totalPricempy(thisdiscount); return totalPricesub(tmpPrice); } public Money getDiscountedDifference() { Money totalPrice = thispricePerCasempy(thisnumberOfCases); return totalPricesub(thisgetCalculatedPrice()); } public int getNumberOfCases() { return numberOfCases; } public void setNumberOfCases(int numberOfCases) { thisnumberOfCases = numberOfCases; } public void setProductType(ProductType productType) { thisproductType = productType; } public String getProductType() { return productTypegetName(); } public void setPricePerCase(Money pricePerCase) { thispricePerCase = pricePerCase; } public Money getPricePerCase() { return pricePerCase; } }


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

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

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

  清單 JUnit 的 WholesaleOrderTest 類

   package orgacmestorediscountenginejunit; import junitframeworkTestCase; import orgacmestoreMoney; import orgacmestorediscountengineWholesaleOrder; public class WholesaleOrderTest extends TestCase { /* * Test method for WholesaleOrdergetCalculatedPrice() */ public void testGetCalculatedPrice() { WholesaleOrder order = new WholesaleOrder(); ordersetDiscount(); ordersetNumberOfCases(); ordersetPricePerCase(new Money()); assertEquals($ ordergetCalculatedPrice()toString()); } /* * Test method for WholesaleOrdergetDiscountedDifference() */ public void testGetDiscountedDifference() { WholesaleOrder order = new WholesaleOrder(); ordersetDiscount(); ordersetNumberOfCases(); ordersetPricePerCase(new Money()); assertEquals($ ordergetDiscountedDifference()toString()); } }


  PricingEngine 類

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

  清單 PricingEngine 類

   package orgacmestorediscountengine; import orgdroolsRuleBase; import orgdroolsWorkingMemory; import orgdroolsioRuleBaseLoader; public class PricingEngine { private static final String RULES=BusinessRulesdrl; private static RuleBase businessRules; private static void loadRules() throws Exception{ if (businessRules==null){ businessRules = RuleBaseLoader loadFromUrl(PricingEngineclassgetResource(RULES)); } } public static void applyDiscount(WholesaleOrder order) throws Exception{ loadRules(); WorkingMemory workingMemory = businessRulesnewWorkingMemory( ); workingMemoryassertObject(order); workingMemoryfireAllRules(); } }


 

  Drools 的規則

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

  清單 BusinessRulesdrl 文件的示例規則

  

   <ruleset name=BusinessRulesSample xmlns= xmlns:java= xmlns:xs=instance xs:schemaLocation= rulesxsd javaxsd> <rule name=st Tier Discount> <parameter identifier=order> <class>WholesaleOrder</class> </parameter> <java:condition>ordergetNumberOfCases() > </java:condition> <java:condition>ordergetNumberOfCases() < </java:condition> <java:condition>ordergetProductType() == yearround</java:condition> <java:consequence> ordersetDiscount(); </java:consequence> </rule> </ruleset>

  標記團隊測試

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

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

   package orgacmestorediscountenginejunit; import junitframeworkTestCase; import orgacmestoreMoney; import orgacmestorediscountenginePricingEngine; import orgacmestorediscountengineProductType; import orgacmestorediscountengineWholesaleOrder; public class DiscountEngineTest extends TestCase { public void testCalculateDiscount() throws Exception{ WholesaleOrder order = new WholesaleOrder(); ordersetNumberOfCases(); ordersetPricePerCase(new Money()); ordersetProductType(ProductTypeYEAR_ROUND); PricingEngineapplyDiscount(order); assertEquals( ordergetDiscount() ); } public void testCalculateDiscountNone() throws Exception{ WholesaleOrder order = new WholesaleOrder(); ordersetNumberOfCases(); ordersetPricePerCase(new Money()); ordersetProductType(ProductTypeSEASONAL); PricingEngineapplyDiscount(order); assertEquals( ordergetDiscount() ); } }


  還沒用 FIT?那就用 FIT!

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

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

  清單 添加 parse 方法到 Money 類

   public static Money parse(String value){ return new Money(DoubleparseDouble(StringUtilsremove(value $))); }


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

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

  清單 測試 Money 類的 parse 方法

   public void testParse() throws Exception{ Money money = Moneyparse($); assertEquals($ moneytoString()); } public void testEquals() throws Exception{ Money money = Moneyparse($); Money control = new Money(); assertEquals(control money); }


  編寫 FIT 裝備

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

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

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

  清單 用 FIT 進行的折扣測試

   package orgacmestorediscount; import orgacmestoreMoney; import orgacmestorediscountenginePricingEngine; import orgacmestorediscountengineProductType; import orgacmestorediscountengineWholesaleOrder; import fitColumnFixture; public class DiscountStructureFIT extends ColumnFixture { public Money listPricePerCase; public int numberOfCases; public boolean isSeasonal; public Money discountPrice() throws Exception { WholesaleOrder order = thisdoOrderCalculation(); return ordergetCalculatedPrice(); } public Money discountAmount() throws Exception { WholesaleOrder order = thisdoOrderCalculation(); return ordergetDiscountedDifference(); } /** * required by FIT for specific types */ public Object parse(String value Class type) throws Exception { if (type == Moneyclass) { return Moneyparse(value); } else { return superparse(value type); } } private WholesaleOrder doOrderCalculation() throws Exception { WholesaleOrder order = new WholesaleOrder(); ordersetNumberOfCases(numberOfCases); ordersetPricePerCase(listPricePerCase); if (isSeasonal) { ordersetProductType(ProductTypeSEASONAL); } else { ordersetProductType(ProductTypeYEAR_ROUND); } PricingEngineapplyDiscount(order); return order; } }


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

   這些結果真的很 FIT !



  結束語

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


From:http://tw.wingwit.com/Article/program/Java/gj/201311/27625.html
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.