JUnit 假定測試的所有方面都是開發人員的地盤而集成測試框架(FIT)在編寫需求的業務客戶和實現需求的開發人員之間做了協作方面的試驗這是否意味著 FIT 和 JUnit 是競爭關系呢?絕對不是!代碼質量完美主義者 Andrew Glover 介紹了如何把 FIT 和 JUnit 兩者最好的地方結合在一起實現更好的團隊工作和有效的端到端測試
在軟件開發的生命周期中每個人都對質量負有責任理想情況下開發人員在開發周期中用像 Junit 和 TestNG 這樣的測試工具保證早期質量而質量保證團隊用功能性系統測試在周期末端跟進使用像 Selenium 這樣的工具但是即使擁有優秀的質量保證有些應用程序在交付的時候仍然被認為是質量低下的為什麼呢?因為它們並沒有做它們應當做的事
在客戶(編寫應用程序需求的)業務部門和(實現需求的)開發團隊之間的溝通錯誤通常是摩擦的原因有時還是開發項目徹底失敗的常見原因幸運的是存在一些方法可以幫助需求作者和實現者之間盡早 溝通
FIT 化的解決方案
集成測試框架 (FIT)是一個測試平台可以幫助需求編寫人員和把需求變成可執行代碼的人員之間的溝通使用 FIT需求被做成表格模型充當開發人員編寫的測試的數據模型表格本身充當輸入和測試的預期輸出
圖 顯示了用 FIT 創建的結構化模型第一行是測試名稱下一行的三列是與輸入(value
和 value
)和預期結果(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
實例成員(value
和 value
)最後一個單元格的值只匹配 TrendIndicator
中的方法(trend
)
現在來看清單 中的 trend
方法它返回一個 String
值可以猜測得到對於表中每個剩下的行FIT 都會替換值並比較結果在這個示例中有三個 數據 行所以 FIT 運行 TrendIndicator
裝備三次第一次value
被設置成 value
設置成 然後 FIT 調用 trend
方法並把從方法得到的值與表中的值比較應當是 decreasing
通過這種方式FIT 用裝備代碼測試 Trender
類每次 FIT 執行 trend
方法時都執行類的 determineTrend
方法當代碼測試完成時FIT 生成如圖 所示的報告
圖 FIT 報告 trend 測試的結果
trend 列單元格的綠色表明測試通過(例如FIT 設置 value
為 value
為 調用 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
實例中設置了折扣就可以通過分別調用 getCalculatedPrice
和 getDiscountedDifference
方法得到折扣價格和節省的錢
更好地測試這些方法(用 JUnit)!
定義了 Money
和 WholesaleOrder
類之後還要編寫 JUnit 測試來驗證 getCalculatedPrice
和 getDiscountedDifference
方法的功能測試如清單 所示
清單 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
的雙親
請注意在清單 中DiscountStructureFIT
的 parse
方法如何比較 class
類型如果存在匹配就調用 Money
的定制 parse
方法否則就調用父類(Fixture
)的 parse
版本
清單 中剩下的代碼是很簡單的對於圖 所示的 FIT 表格中的每個數據行都設置值並調用方法然後 FIT 驗證結果!例如在 FIT 測試的第一次運行中DiscountStructureFIT
的 listPricePerCase
被設為 $numberOfCases
設為 isSeasonal
為 true然後執行 DiscountStructureFIT
的 discountPrice
返回的值與 $ 比較然後執行 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