2010-01-24 40 views
7

我的公司在單元測試中,我在重構服務層代碼時遇到了一些問題。下面是一些代碼,我寫了一個例子:重構服務層類

public class InvoiceCalculator:IInvoiceCalculator 
{ 
    public CalculateInvoice(Invoice invoice) 
    { 
     foreach (InvoiceLine il in invoice.Lines) 
     { 
      UpdateLine(il); 
     } 
     //do a ton of other stuff here 
    } 

    private UpdateLine(InvoiceLine line) 
    { 
     line.Amount = line.Qty * line.Rate; 
     //do a bunch of other stuff, including calls to other private methods 
    } 
} 

在這個簡化的情況下(這是從有1分公共法和〜30倍私立學校的1000線類下降),我的老闆說我應該能夠分別測試我的CalculateInvoice和UpdateLine(UpdateLine實際上調用3個其他私有方法,並執行數據庫調用)。但是,我會如何做到這一點?他提出的重構似乎有點令人費解對我說:

//Tiny part of original code 
public class InvoiceCalculator:IInvoiceCalculator 
{ 
    public ILineUpdater _lineUpdater; 

    public InvoiceCalculator (ILineUpdater lineUpdater) 
    { 
     _lineUpdater = lineUpdater; 
    } 

    public CalculateInvoice(Invoice invoice) 
    { 
     foreach (InvoiceLine il in invoice.Lines) 
     { 
      _lineUpdater.UpdateLine(il); 
     } 
     //do a ton of other stuff here 
    } 
} 

public class LineUpdater:ILineUpdater 
{ 
    public UpdateLine(InvoiceLine line) 
    { 
     line.Amount = line.Qty * line.Rate; 
     //do a bunch of other stuff 
    } 
} 

我能看到的依賴是如何打破現在,我可以測試片,但這樣做也從我的原班製作20-30額外的課程。我們只在一個地方計算髮票,因此這些部分不會真正可重複使用。這是做出這個改變的正確方法,還是你建議我做一些不同的事情?

謝謝!

Jess

回答

1

IMO這一切都取決於 '顯著' 那UpdateLine()方法確實是。如果它只是一個實現細節(例如,它可以很容易地在CalculateInvoice()方法內部進行內聯,並且它們只會損害可讀性),那麼您可能不需要單獨對主類進行單獨測試。另一方面,如果UpdateLine()方法對業務邏輯具有一定的價值,如果您可以想象當您需要獨立於其餘類別更改此方法的情況(並因此單獨對其進行測試),那麼可以使用UpdateLine()那麼你應該繼續重構它到一個單獨的LineUpdater類。

你可能不會以這種方式得到20-30個類,因爲大多數這些私有方法實際上只是實現細節,並不值得單獨測試。

0

是的,很好的一個。我不確定InvoiceLine對象是否也包含一些邏輯,否則,您可能還需要一個IInvoiceLine。 我有時會遇到同樣的問題。一方面,你希望做正確的事情並且單元測試你的代碼,但是當涉及數據庫調用和可能的文件寫入時,它會導致大量額外的工作來設置第一個測試,所有測試對象在文件寫入和數據庫io涉及發生,接口,斷言,並且您還想測試數據層是否包含任何錯誤。因此,更「過程」和「單位」的測試通常更容易構建。 如果你有一個項目會被改變很多(將來)和這個代碼的很多依賴項(也許其他程序讀取文件或數據庫數據),那麼對你的所有部分進行穩定的單元測試可能會很好代碼,投資時間是值得的。 但是如果這個項目像我最近的客戶那樣說'讓我們活下去吧,也許我們會在明年和明年調整一下會有新的東西',比我自己不會那麼努力去得到所有人單元測試正在運行。

米歇爾

1

那麼,你的老闆在單元測試方面更加正確:
他現在能夠在不測試UpdateLine()函數的情況下測試CalculateInvoice()。他可以傳遞模擬對象而不是真正的LineUpdater對象,並只測試CalculateInvoice(),而不是一大堆代碼。
是不是?這取決於。你的老闆想要進行真正的單元測試。在你的第一個例子中測試不是單元測試,而是集成測試。

集成測試之前單元測試的優點是什麼?
1)單元測試允許您只測試一個方法或屬性,而不受其他方法/數據庫等的影響。
2)第二個優點 - 單元測試執行速度更快(例如,您說UpdateLine使用數據庫),因爲它們不測試所有嵌套的方法。嵌套方法可以是數據庫調用,所以如果你有數千次測試,你的測試可能會運行緩慢(幾分鐘)。
3)第三個好處:如果你的方法進行數據庫調用,那麼有時你需要設置數據庫(填充測試所需的數據),這可能不容易 - 也許你必須編寫幾頁代碼只是爲了準備測試數據庫。使用單元測試,可以將數據庫調用與正在測試的方法分開(使用模擬對象)。

但是!我並不是說單元測試更好。他們只是不同。正如我所說的,單元測試允許您快速隔離地測試一個單元。集成測試更容易,並允許您測試不同方法和圖層的聯合工作結果。老實說,我更喜歡集成測試:)

另外,我有幾個建議給你:
1)我不認爲有金額字段是一個好主意。看起來「金額」字段是額外的,因爲它的值可以根據其他2個公共字段進行計算。如果你想這樣做,我會把它作爲只讀屬性返回Qty * Rate。
2)通常,擁有一個由1000行組成的類可能意味着它設計得不好,應該重構。

現在,我希望你更好地瞭解情況,並可以決定。此外,如果你瞭解情況,你可以與你的老闆交談,你可以一起決定。

0

你老闆的例子對我來說看起來很合理。

一些我嘗試設計的任何方案時要記住的關鍵因素是:

  1. 單一職責原則

    類應該只改變的原因之一。

  2. 是否每個新類證明它的存在

    已創建只是爲了它的緣故類,或者說它們封裝邏輯的一個有意義的部分?

  3. 你能夠獨立測試每段代碼嗎?

在您的方案,只是看着名字,這樣看來,你是漂泊單一職責了 - 你有一個IInvoiceCalculator,但這個類是則還負責更新InvoiceLines。您不僅使測試更新行爲變得非常困難,而且當計算業務規則更改時,您需要更改InvoiceCalculator類。

然後有關於更新邏輯的問題 - 邏輯證明一個單獨的類是否合理?這真的很有依賴性,而且很難說沒有看到代碼,但肯定是你的老闆想要被測邏輯的事實表明,它不僅僅是一個簡單的在班輪上呼叫數據層。

你說這個重構會創建大量額外的類(我在所有業務實體中都採用這種方式,因爲我只在您的示例中看到了幾個新類和它們的接口),但是您有考慮你從中得到什麼。看起來您可以獲得完整的代碼測試能力,能夠獨立引入新的計算和新的更新邏輯,並且可以更清晰地封裝單獨的業務邏輯。

增益當然要進行成本效益分析,但是由於您的噓聲正在尋求它們,因此聽起來他很高興他們會付出代價,反對通過這種方式執行代碼的額外工作。

關於獨立測試的最後第三點也是您的老闆設計的一個關鍵好處 - 公共方法越接近執行實際工作的代碼,注入樁或嘲笑系統中未經測試的部分。例如,如果您正在測試關閉數據層的更新方法,則不需要測試數據層,因此您通常會注入一個模擬。如果您需要首先通過所有計算器邏輯傳遞模擬數據層,那麼您的測試設置將變得複雜得多,因爲模擬現在需要滿足許多其他潛在需求,而與有關的實際測試無關。

雖然這種方法最初是額外的工作,但我認爲大部分工作是考慮設計的時候,在此之後,以及在掌握更多基於注入的代碼風格之後,以這種方式構建的軟件的原始實施時間實際上是可比較的。

5

這是Feature Envy一個例子:

line.Amount = line.Qty * line.Rate; 

應該看起來可能更像:

var amount = line.CalculateAmount(); 

沒有什麼毛病很多小的類,它不是關於重用性就像它的適應性一樣。如果您有許多單一職責類別,則可以更輕鬆地查看系統的行爲,並在需求更改時更改它。大班有相互纏繞的責任,這使得很難改變。

+0

我們所有的實體對象基本都是DTO,所以所有的業務邏輯都在服務類中。但是在發票行計算器中有很多其他邏輯,我只是​​展示了一件。 – 2010-01-24 21:19:19

0

你hoss的方法是依賴注入的一個很好的例子,以及如何這樣做可以讓你使用模擬ILineUpdater有效地進行測試。