構建的系統架構能讓大多數測試用例不依賴於資料庫就能執行,這樣最好了,但是我們還是經常遇到許多測試用例需要資料庫的。在這種情況下,我們可以擴充套件測試自動化框架(test automation framework)去完成大部份的工作。可以增加一種使用框架來進行建立物件註冊的辦法,這樣,框架就能為進行刪除操作。
首先,當建立物件時,我們需要註冊它。
// set up fixture
billingaddress = new address("1222 1st st sw", "calgary",
"alberta", "t2n 2v2", "canada");
registertestobject(billingaddress);
shippingaddress = new address("1333 1st st sw", "calgary",
"alberta","t2n 2v2", "canada");
registertestobject(shippingaddress);
customer = new customer(99, "john", "doe",
new bigdecimal("30"),
billingaddress,
shippingaddress);
registertestobject(shippingaddress);
product = new product(88, "somewidget",
new bigdecimal("19.99"));
registertestobject(shippingaddress);
invoice = new invoice(customer);
registertestobject(shippingaddress);
註冊過程由將物件加入到測試物件集合的操作構成:
list testobjects;
protected void setup() throws exception
protected void registertestobject(object testobject)
在teardown方法中,我們遍歷測試物件集合,並將每個物件刪除:
public void teardown() catch (runtimeexception e) }}
現在測試用例變成這樣子:
public void testadditemquantity_severalquantity_v8()
我們已經能夠將try/finally語句塊給移除。除了呼叫registertestobject外,我們變得**簡單多了。但我們仍然可以進一步精簡**。為什麼這樣說?我們需要將宣告變數,然後將它們初始化為null,再稍後對其重新初始化嗎?之前的測試用例這樣做是因為這樣變數必須在finally語句塊中可訪問;現在我們將finally塊移除了,所以我們可以將變數定義與初始化操作合併。
public void testadditemquantity_severalquantity_v9()
3) 清理夾具建立
我們已經清理好了斷言及夾具解除安裝,現在來看看夾具建立。乙個明顯的「快速修復」就是,對建構函式和registertestobject的呼叫,利用「方法抽取重構」來定義「生成方法」(creation method)。這樣可以使得測試用例更易於讀寫。「生成方法」有另外乙個好處:它們封裝了sut的api,使得當物件的建構函式發生改變時,我們不必每個測試用例都去更改,只需要去修改乙個地方,減少了測試用例的維護成本。
public void testadditemquantity_severalquantity_v10()
這個夾具建立邏輯還是有其他問題。第乙個問題是,很難明確這個夾具與測試預期輸出之前的聯絡。customer物件的細節會以某種方式影響到輸出嗎?customer的address域會影響到輸出?這個用例真正想驗證的是什麼?
另乙個問題是:這個測試用例展示了「硬編碼測試資料」(hard-coded test data)(見模糊測試)。如果sut將我們建立的所有物件持久化到資料庫中,那硬編碼資料就會導致:當customer,product或者invoice的某些域要求必須唯一時,會出現「不可重覆式測試」、「互動式測試」或者「測試執行衝突test run war」(見不穩定測試)。
我們可以通過為每乙個測試用例生成唯一的值並用這個值做為種子(seed)去產生用例中使用到的物件。這個辦法能保證每次用例執行時,都會得到不同的物件。因為我們已經將物件產生邏輯移到了生成方法中,這一步修改相對容易。我們只要將上述邏輯放到生成方法中並去掉相應的引數就行了。抽取方法式重構還有另外乙個用處,我們可以生成乙個新的、無引數的新版生成方法。
public void testadditemquantity_severalquantity_v11()
private product createaproduct(bigdecimal unitprice)
我們將這個模式稱為「匿名生成方法」(anonymous creation method),因為這個模式表明我們並不關心物件本身的特性。如果sut的預期行為依賴於特定的值,我們要麼可以將這個值做為引數傳到生成函式,要麼可以在生成函式的函式名中暗示。
這個測試用例看上會好一些了,但是仍然沒有做完。預期結果真的以某種方式依賴於customer物件的address?如果不依賴,我們可以通過抽取方法式重構(再一次)將它們的建立過程完全隱藏,這裡用createacustomer方法來達到這個目的:
public void testadditemquantity_severalquantity_v12()
將建立address物件的呼叫移到customer的建立方法中,我們更加清楚:這個測試用例中,address物件並不會影響我們需要驗證的邏輯。然而,結果依賴於customer的discount,所以將discount做為customer的生成函式的引數。
我們還是有一兩個地方要處理的。比如,表示單價(unit price),數量(quantity),折扣(discount)的測試資料是硬編碼的,且在這個測試用例中重複出現了兩次。我們可以使用「用符號化常量替換魔數式重構」來給這些資料以名稱,用於說明它們的含意。另外,用於建立lineitem物件的建構函式並沒有在sut本身的其他地方使用到,因為lineitem只是在其建立時去計算extenedcost。我們可以將這個針對測試本身的**放到測試輔助中的外部方法(foreign method[fowler])。我們已經在處理cuostom及product中看到了例子:使用引數化的生成方法,只根據相關的值返回預期的的lineitem物件。
public void testadditemquantity_severalquantity_v13()
4) 清理後的測試用例
下面是清理後的最終版本的測試用例:
public void testadditemquantity_severalquantity_v14()
我們已經使用了「引入解釋變數式重構」將base_price(price*quantity)和extended_price(帶折扣的**)的計算過程更好地文件化了。修訂過的測試用例比我們開始時面對的笨重的**各小巧、各清晰。它也實現了「測試即文件」的任務。那麼,我們能發現這個測試驗證的是什麼嗎?它驗證了通過新增函式,商品(item)確實回到了發貨單(invoice),而且總成本(extended cost)是基於產品**(product price),使用者的折扣(coustomer』s discount)及預定的數量(quantity ordered)。
0.4 寫更多的測試用例
看上去我們花了很大的精力去重構這個用例以使其更加清晰,是不是每乙個用例都要花費這麼多的精力呢?
希望不是!這兒花費的努力大多數與發現用例中需要用到的測試輔助方法(test utility methods)有關。我們定義了測試我們程式的「更高階語言」(higher-level language)。一旦有了這些輔助方法,編寫其他用例就會變得簡單得多。比如,如果我們想去驗證當lineitem的數量發生變化時,總**(extended cost)會被重新計算,我們就可以重用大多數的測試輔助方法。
public void testaddlineitem_quantityone()
public void testchangequantity_severalquantity()
這個用例只要了大概兩分鐘,而且沒有去新增任何新的測試輔助方法。比較一下,如果按照之前的風格來寫乙個全新的測試用例,會要多長的時間。另外,在測試用例編寫上面節省的精力只是等式的一部份,還應該考慮到,每次重訪已有測試用例時,可以節省理解的時間。在專案開發及後續的維護活動中,這種節省還會是不斷累積的。
0.5 進一步的精簡
後來新增的測試用例帶來了**重複。比如,我們總會去建立customer和invoice物件。為什麼不能將這兩行合併呢?同理,我們在測試方法中不斷定義、初始化常量quantity及customer_discount_pc。為什麼不能只做一次?pruduct物件沒有在測試中發揮作用,我們總是用同樣方式去建立它。能將這個職責提出來嗎?當然可以,我們對每組重複**使用「抽取方法式重構」,建立乙個更強大的「生成方法」。
public void testadditemquantity_severalquantity_v15()
public void testaddlineitem_quantityone_v2()
public void testchangequantity_severalquantity_v2()
我們將需要去理解的35行**減少到6行。只需要去維護原有**的1/6。可以進一步將夾具的建立放到setup方法中,不過只有將很多測試用例都需要同樣的customer/discout/invoice的配置。如果我們想讓其他測試用例類(testcase classes)復用這樣測試輔助方法,我們可以使用「抽取超類式重構」建立乙個測試用例超類(testcase superclass),再使用「上移方法式重構」將這些測試輔助函式上移,那它們就能被重用了。
如何重構乙個系統
發現乙個很有意思的情況,做系統寫 多年了,遇到的需求基本上是在已有的系統上實現,從頭來實現的系統基本上沒有。無論是從頭是實現乙個系統,還是維護乙個系統,當時實現的技術可能是最先進的 規劃的產品邏輯是合理的,隨著時間的發展 開發人員的變更 系統的 質量會逐漸腐化,加個feature太麻煩,改個bug涉...
《重構》讀書筆記(二) 第一章 第乙個重構案例
作者在第一章通過乙個影片出租的例子,試圖闡述重構的基本過程和步驟。看得出來,作者對這個案例給予厚望,花了很大的篇幅。正因如此,我沒有理由不好好學習這一章。影片出租的例子本身不難,但我足足花了一整個下午學習了這個例子。我先是老老實實的把 用c 重抄了一遍,然後跟著作者的步伐,一步步重構,以期體驗 重構...
乙個專案工程的重構小結
首次看到移交到我手上的c 工程那個7千多行 的檔案時,既佩服又擔心,前輩的構架能力讓我甘拜下風,同時擔心能不能勝任。在研究原始碼的時候,也解決幾個線上遺留的問題。愈發覺得有重構的必要 因為這將是我負責的 不完全重構 那叫重寫,一是否定了前輩的功勞,二是接觸不久業務不熟,風險大。因此總體原則是逐步重構...