2011-04-13 55 views
7

我在當前正在開發的MVC 3應用程序中使用存儲庫模式。我的倉庫界面看起來如下:NSubstitute - 測試特定的linq表達式

public interface IRepository<TEntity> where TEntity : IdEntity 
{ 
    void Add(TEntity entity); 
    void Update(TEntity entity); 
    void Remove(TEntity entity); 
    TEntity GetById(int id); 
    IList<TEntity> GetAll(); 
    TEntity FindFirst(Expression<Func<TEntity, bool>> criteria); 
    IList<TEntity> Find(Expression<Func<TEntity, bool>> criteria); 
} 

在許多實例中,當我在服務類的編碼方法,我使用的是使用FindFirst和查找方法。正如你所看到的,它們都以linq表達式作爲輸入。我想知道的是,NSubstitute是否允許您在代碼中指定要測試的特定表達式。

所以,這裏是說明了使用存儲庫的方法之一的我已經提到的服務方法的一個例子:

public IList<InvoiceDTO> GetUnprocessedInvoices() 
    { 
     try 
     { 
      var invoices = _invoiceRepository.Find(i => !i.IsProcessed && i.IsConfirmed); 
      var dtoInvoices = Mapper.Map<IList<Invoice>, IList<InvoiceDTO>>(invoices); 
      return dtoInvoices; 
     } 
     catch (Exception ex) 
     { 
      throw new Exception(string.Format("Failed to get unprocessed invoices: {0}", ex.Message), ex); 
     } 
    } 

那麼,有沒有一種方法,使用Nsubtitute,我可以測試具體lamda表達式:(i =>!i.IsProcessed & & i.IsConfirmed)?

任何指導將不勝感激。

回答

12

非常簡短的回答是否定的,NSubstitute沒有任何內置它使測試具體表現更加容易。

長得多的答案是有,你可以嘗試幾個選項,其中大部分涉及避免被測類直接使用LINQ的。我不確定這些是否是好主意,因爲我不知道整個背景,但希望在這裏可以使用一些信息。在下面的例子中,我已經消除了Mapper步驟,以使代碼樣本更小一些。

第一種選擇是讓這樣你就可以檢查表達式是你期望的相同的參考,這意味着你不能再在你的代碼中直接創建測試。例如:

//Class under test uses: 
_invoiceRepository.Find(Queries.UnprocessedConfirmedOrders) 

[Test] 
public void TestUnprocessedInvoices() 
{ 
    IList<InvoiceDTO> expectedResults = new List<InvoiceDTO>(); 
    _invoiceRepository.Find(Queries.UnprocessedConfirmedOrders).Returns(expectedResults); 
    Assert.That(_sut.GetUnprocessedInvoices(), Is.SameAs(expectedResults)); 
} 

我已經將表達式轉儲到靜態查詢類上,但您可以使用工廠來更好地封裝它。由於您使用的是實際表達式的引用,因此可以設置返回值並檢查正常接收的調用。您也可以單獨測試表達式。

第二個選項通過使用規格模式進一步考慮了這一點。說你下面的成員添加到IRepository接口,並引入ISpecification:

public interface IRepository<TEntity> where TEntity : IdEntity 
{ 
    /* ...snip... */ 
    IList<TEntity> Find(ISpecification<TEntity> query); 
} 

public interface ISpecification<T> { bool Matches(T item); } 

然後,您可以測試它是這樣的:

//Class under test now uses: 
_invoiceRepository.Find(new UnprocessedConfirmedOrdersQuery()); 

[Test] 
public void TestUnprocessedInvoicesUsingSpecification() 
{ 
    IList<InvoiceDTO> expectedResults = new List<InvoiceDTO>(); 
    _invoiceRepository.Find(Arg.Any<UnprocessedConfirmedOrdersQuery>()).Returns(expectedResults); 
    Assert.That(_sut.GetUnprocessedInvoices(), Is.SameAs(expectedResults)); 
} 

同樣,你可以隔離測試此查詢,以確保它做你的想法。

第三種方法是捕捉使用的參數並直接對其進行測試。這是一個有點亂,但工程:

[Test] 
public void TestUnprocessedInvoicesByCatchingExpression() 
{ 
    Expression<Func<InvoiceDTO, bool>> queryUsed = null; 
    IList<InvoiceDTO> expectedResults = new List<InvoiceDTO>(); 
    _invoiceRepository 
     .Find(i => true) 
     .ReturnsForAnyArgs(x => 
     { 
      queryUsed = (Expression<Func<InvoiceDTO, bool>>)x[0]; 
      return expectedResults; 
     }); 

    Assert.That(_sut.GetUnprocessedInvoices(), Is.SameAs(expectedResults)); 
    AssertQueryPassesFor(queryUsed, new InvoiceDTO { IsProcessed = false, IsConfirmed = true }); 
    AssertQueryFailsFor(queryUsed, new InvoiceDTO { IsProcessed = true, IsConfirmed = true }); 
} 

(希望這將讓未來NSubstitute版本更容易一點)

第四個選項是要找到/借/寫/竊取一些代碼,可以比較表達式樹,並使用NSubstitute的Arg.Is(...)來取得一個謂詞來比較表達式樹。

第五種方法是不單元測試它到那個程度,而只是使用真正的InvoiceRepository進行集成測試。不要擔心所發生的事情的機制,請嘗試驗證您所需的實際行爲。

我的一般建議是看看你需要測試什麼,以及如何才能最好,最容易編寫這些測試。請記住,表達式和它傳遞的事實都需要以某種方式進行測試,並且測試不需要是單元測試。也許值得考慮一下,當前的IRepository接口是否讓你的生活更輕鬆。您可以嘗試編寫測試,比如,然後查看可以推出哪些設計來支持可測試性。

希望這會有所幫助。

+0

我一直在摔跤類似的東西 - 我真的很喜歡能夠測試我所有的LINQ,但我發現很難,當你介紹這樣的事情不能被執行SqlFunctions。 http://stackoverflow.com/questions/21495980/testing-sqlfunctions-in-linq-statements - 我想我只是去集成測試,而不是 – Neil 2014-02-01 09:21:16

3

當我試圖找出如何在NSubstitute中使用lambda表達式返回特定值時,我偶然發現了這個問題。但是,對於我的用例,我不在乎實際傳遞給linq查詢的內容,並且想要分享如何在NSubstitute中的模擬接口上返回linq查詢的值。

因此,使用從上面

[Test] 
public void TestUnprocessedInvoices() 
{ 
    IList<InvoiceDTO> expectedResults = new List<InvoiceDTO>(); 
    _invoiceRepository.Find(Arg.Any<Expression<Func<Invoice, bool>>>()).Returns(expectedResults); 
} 
3

我不願放棄對我的倉庫界面使用Expression<Func<T,bool>>,所以作爲替代編程這一個特定的模擬(因爲NSubstitute不支持的例子它),我只是在測試夾具內創建了一個私有類,它實現了我的存儲庫接口,並且只創建了測試將使用的與表達式相關的方法。我可以像往常一樣繼續使用NSubstitute來模擬所有其他依賴項,但是我可以使用同一個存儲庫進行多個不同的測試,並從不同的輸入中獲得不同的結果。

public class SomeFixture 
{ 
    private readonly IRepository<SomeEntity> entityRepository; 
    private readonly IRepository<SomeThing> thingRepository; 

    public SomeFixture() 
    { 
     var entities = new List<SomeEntity> 
     { 
      BuildEntityForThing(1), 
      BuildEntityForThing(1), 
      BuildEntityForThing(1), 
      BuildEntityForThing(2), 
     }; 
     entityRepository = new FakeRepository(entities); 

     thingRepository = Substitute.For<IRepository<SomeThing>>(); 
     thingRepository.GetById(1).Returns(BuildThing(1)); 
     thingRepository.GetById(2).Returns(BuildThing(2)); 
    } 

    public void SomeTest() 
    { 
     var classUnderTest = new SomeClass(thingRepository, entityRepository); 

     Assert.AreEqual(classUnderTest.FetchEntitiesForThing(1).Count, 3); 
    } 

    private void SomeOtherTest() 
    { 
     var classUnderTest = new SomeClass(thingRepository, entityRepository); 

     Assert.AreEqual(classUnderTest.FetchEntitiesForThing(2).Count, 1); 
    } 

    private class FakeRepository : IRepository<SomeEntity> 
    { 
     private readonly List<SomeEntity> items; 

     public FakeRepository(List<SomeEntity> items) 
     { 
      this.items = items; 
     } 

     IList<TEntity> Find(Expression<Func<SomeEntity, bool>> criteria) 
     { 
      // For these purposes, ignore possible inconsistencies 
      // between Linq and SQL when executing expressions 
      return items.Where(criteria.Compile()).ToList(); 
     } 

     // Other unimplemented methods from IRepository ... 
     void Add(SomeEntity entity) 
     { 
      throw new NotImplementedException(); 
     } 
    } 
}