2010-11-05 126 views
2

在我的ASP.Net MVC應用程序中,我使用IoC來簡化單元測試。我的應用程序的結構是Controller -> Service Class -> Repository類型的結構。爲了做單元測試,我有一個InMemoryRepository類繼承我的IRepository,而不是去數據庫,它使用內部List<T>成員。當我構建我的單元測試時,我只傳遞一個內部存儲庫的實例,而不是我的EF存儲庫。單元測試時我應該使用模擬對象嗎?

我的服務類通過我的存儲庫類實現的AsQueryable接口從存儲庫檢索對象,因此允許我在沒有服務類的情況下在我的服務類中使用Linq,同時仍然抽象出數據訪問層。在實踐中,這似乎運作良好。

我看到的問題是,每當我看到單元測試談到,他們都使用模擬對象,而不是我看到的內部方法。在面值上它是有道理的,因爲如果我的InMemoryRepository失敗,不僅我的InMemoryRepository單元測試失敗,但是這個失敗也會級聯到我的服務類和控制器中。更現實的是,我更關心影響控制器單元測試的服務類中的故障。

我的方法還要求我爲每個單元測試做更多的設置,並且隨着事情變得更加複雜(例如,我在服務類中實現授權),設置變得更加複雜,因爲我必須確保每個單元測試會正確授予它與服務類別,因此該單元測試的主要方面不會失敗。我可以清楚地看到模擬對象如何在這方面提供幫助。

但是,我不明白如何解決這個完全與嘲笑,仍然有效的測試。例如,我的一個單元測試是,如果我調用_service.GetDocumentById(5),它會從存儲庫中獲取正確的文檔。這是一個有效的單元測試的唯一方法(據我瞭解)是如果我有2或3個文件存儲,並且我的GetdocumentById()方法正確地檢索Id爲5的那個。

我將如何有一個嘲笑庫AsQueryable調用,以及如何確保我沒有掩蓋任何問題,我用我的Linq語句硬編碼設置模擬存儲庫時的返回語句?使用InMemoryRepository保持我的服務類單元測試更好嗎,但將控制器單元測試更改爲使用模擬服務對象?


編輯: 去在我的結構之後再次我記得是防止控制器單元測試嘲諷的併發症,因爲我忘了我的結構比我原來說有點複雜。

A Repository是一種對象類型的數據存儲,所以如果我的文檔服務類需要文檔實體,它會創建一個IRepository<Document>

控制器通過IRepositoryFactoryIRepositoryFactory是一個類,它可以很容易地創建存儲庫,而無需將存儲庫直接存入控制器,或讓控制器擔心哪些服務類需要哪些存儲庫。我有一個InMemoryRepositoryFactory,它給出了服務類別InMemoryRepository<Entity>實例化,並且我的EFRepositoryFactory也有同樣的想法。

在控制器的構造函數中,通過傳入傳入該控制器的IRepositoryFactory對象來實例化私有服務類對象。

因此,例如

public class DocumentController : Controller 
{ 
    private DocumentService _documentService; 

    public DocumentController(IRepositoryFactory factory) 
    { 
     _documentService = new DocumentService(factory); 
    } 
    ... 
} 

我不能看到如何使用這種架構嘲笑我的服務層,使我的控制器單元測試,不融合測試。我可能有一個糟糕的單元測試架構,但我不確定如何更好地解決讓我想要首先創建一個存儲庫工廠的問題。

+2

「使用InMemoryRepository'保持我的服務類單元測試更好,但更改我的控制器單元測試以使用模擬服務對象?」對,就是這樣。 – 2010-11-05 14:56:22

+0

我剛剛更新了這個問題,在測試控制器對我來說看起來不太明顯的時候,爲什麼嘲笑服務層更加詳細。 – KallDrexx 2010-11-05 15:24:13

回答

3

一個解決問題的方法是改變你的控制器的需求,而不是構建服務本身IDocumentService實例:

public class DocumentController : Controller 
{ 
    private IDocumentService _documentService; 

    // The controller doesn't construct the service itself 
    public DocumentController(IDocumentService documentService) 
    { 
     _documentService = documentService; 
    } 
    ... 
} 

在您的實際應用,讓你的IoC容器注入IRepositoryFactory實例爲您服務。在您的控制器單元測試中,只需根據需要嘲笑服務。

(而看到Misko Hevry's article about constructors doing real work爲重組你的代碼這樣的好處的進一步討論。)

+0

謝謝。這很有意義,您提供的鏈接也是如此! – KallDrexx 2010-11-05 18:10:55

1

我認爲你的方法對於測試服務層本身是合理的,但是,正如你所建議的那樣,如果服務層完全嘲笑你的業務邏輯和其他高級測試會更好。這使得您的高級測試更易於實施/維護,因爲如果測試已經過測試,則無需再次執行服務層。

2

就個人而言,我會設計圍繞工作模式的引用庫該股系統。這可以使事情變得更簡單,並允許您以原子方式運行更復雜的操作。您通常會有一個IUnitOfWorkFactory作爲服務類中的依賴項提供。服務類將創建一個新的工作單元,並且該工作單元引用存儲庫。你可以看到這樣一個例子:here

如果我理解正確,您會關注一個(低級別)代碼中的錯誤,導致很難查看實際問題。你拿InMemoryRepository作爲一個具體的例子。

雖然你的關注是有效的,但我個人不會擔心失敗InMemoryRepository。它是一個測試對象,您應該儘可能簡化這些測試對象。這可以防止您必須爲測試對象編寫測試。大多數情況下,我認爲他們是正確的(但是,我有時通過編寫Assert語句在這樣的類中使用自檢)。當這樣的對象出現故障時,測試將失敗。這不是最佳的,但你通常會很快地發現問題在我的經驗中。要提高生產力,您必須在某處繪製一條線。

服務器引起的控制器錯誤是另一杯茶IMO。雖然你可以嘲笑這項服務,但這會使測試變得更加困難和不太可靠。最好不要測試服務。只測試控制器!控制器會調用服務,如果你的服務表現不好,你的控制器測試會發現。這樣你只能測試應用程序中的頂級對象。代碼覆蓋範圍將幫助您找到您未測試的部分代碼。當然,這在所有場景中都是不可能的,但這通常運作良好。當服務與模擬存儲庫(或工作單元)一起工作時,這將工作得很好。


你的第二個問題是那些depedencies讓你有多少測試設置。對此我有兩件事要說。

首先,我嘗試我的依賴倒置減少到只有我需要能夠運行我的單元測試。調用系統時鐘,數據庫,Smtp服務器和文件系統應該是僞造的,以使單元測試快速可靠。其他事情我儘量不要反轉,因爲你越嘲笑,測試變得越不可靠。你正在測試更少。最小化的依賴反轉(你有什麼需要有良好的RTM單元測試)有助於使測試設置更容易。

但(第二點),你還需要編寫單元測試的方式,他們的可讀性和可維護性(單元測試最困難的部分,其實還是製作軟件一般)。當一個類獲得新的依賴關係時,具有較大的測試設置會讓他們難以理解,並使得測試代碼難以改變。我發現的讓測試更具可讀性和可維護性的最佳方法之一是使用簡單的工廠方法在測試類集中的,你在測試(我從來沒有使用嘲諷框架)需要類型的創建。我使用了兩種模式。一個是一個簡單的工廠方法,如一個,創建一個有效的類型:

FakeDocumentService CreateValidService() 
{ 
    return CreateValidService(CreateInitializedContext()); 
} 

FakeDocumentService CreateValidService(InMemoryUnitOfWork context) 
{ 
    return new FakeDocumentSerice(context); 
} 

這樣的測試可以簡單地調用這些方法時,他們需要一個有效的對象,他們只需調用的工廠方法之一。當然,當這些方法之一意外地創建了一個無效的對象時,許多測試都會失敗。這很難防止,但很容易修復。容易修復意味着測試是可維護的。

我用另一種方式是使用保存的參數/你要創建的實際對象的屬性的容器類型。當對象具有許多不同的屬性和/或構造函數參數時,這會特別有用。有一個工廠的容器和對象來創建一個構建器方法混合本,你會得到非常可讀的測試代碼:

[TestMethod] 
public void Operation_WithValidArguments_Succeeds() 
{ 
    // Arrange 
    var validArgs = CreateValidArgs(); 

    var service = BuildNewService(validArgs); 

    // Act 
    service.Operation(); 
} 

[TestMethod] 
[ExpectedException(typeof(InvalidOperationException))] 
public void Operation_NegativeAge_ThrowsException() 
{ 
    // Arrange 
    var invalidArgs = CreateValidArgs(); 

    invalidArgs.Age = -1; 

    var service = BuildNewService(invalidArgs); 

    // Act 
    service.Operation(); 
} 

這樣就可以讓測試只能指定哪些事項!這對於使測試可讀是非常重要的! CreateValidArgs()方法可以創建一個具有超過100個參數的容器,這些參數將會生成一個有效的SUT(被測系統)。你現在集中在一個地方默認的有效配置。我希望這是有道理的。


你的第三個問題約爲未能測試,如果LINQ查詢與給定的LINQ提供程序的行爲果然。這是一個有效的問題,因爲編寫LINQ(對於表達式樹)查詢非常容易,這些查詢在通過內存對象使用時可以完美運行,但在查詢數據庫時會失敗。有時候,翻譯查詢是不可能的(因爲你調用了一個沒有對應數據庫的.NET方法),或者LINQ提供者有限制(或者錯誤)。特別是實體框架3.5的LINQ提供者很難。

但是,這是你無法定義每個單元測試解決問題。因爲當你在測試中調用數據庫時,它不再是單元測試。然而,單元測試從來沒有完全取代手動測試:-)

不過,這是一個值得關注的問題。除了單元測試外,您還可以進行集成測試。在這種情況下,您可以使用真正的提供程序和(專用)測試數據庫運行代碼。在數據庫事務中運行每個測試,並在測試結束時回滾事務(TransactionScope適用於此!)。但是請注意,編寫可維護的集成測試比編寫可維護的單元測試更困難。你必須確保你的測試數據庫的模型是同步的。每個集成測試都應該在數據庫中插入該測試所需的數據,這通常需要編寫和維護很多工作。最好的做法是儘量減少集成測試的數量。有足夠的集成測試讓您對系統進行更改有信心。例如,必須在單個測試中調用帶有複雜LINQ語句的服務方法,通常足以測試您的LINQ提供程序是否能夠構建有效的SQL。大多數時候我只是假設LINQ提供者將具有與LINQ to Objects(.AsQueryable())提供者相同的行爲。再一次,你將不得不在某處畫線。

我希望這會有所幫助。

相關問題