2014-12-05 32 views
1

我想用NSubstitute通過模擬DbSet來單元測試Entity Framework 6.x。幸運的是,Scott Xu提供了一個很好的單元測試庫,EntityFramework.Testing.Moq使用Moq。所以,我修改了自己的代碼以適合NSubstitute,直到我想測試DbSet<T>.Add(),DbSet<T>.Remove()方法爲止,它的外觀一直很好。這裏是我的代碼位:使用DbSet處理對象<T>和IQueryable <T>使用NSubstitute返回錯誤

public static class NSubstituteDbSetExtensions 
{ 
    public static DbSet<TEntity> SetupData<TEntity>(this DbSet<TEntity> dbset, ICollection<TEntity> data = null, Func<object[], TEntity> find = null) where TEntity : class 
    { 
    data = data ?? new List<TEntity>(); 
    find = find ?? (o => null); 

    var query = new InMemoryAsyncQueryable<TEntity>(data.AsQueryable()); 

    ((IQueryable<TEntity>)dbset).Provider.Returns(query.Provider); 
    ((IQueryable<TEntity>)dbset).Expression.Returns(query.Expression); 
    ((IQueryable<TEntity>)dbset).ElementType.Returns(query.ElementType); 
    ((IQueryable<TEntity>)dbset).GetEnumerator().Returns(query.GetEnumerator()); 

#if !NET40 
    ((IDbAsyncEnumerable<TEntity>)dbset).GetAsyncEnumerator().Returns(new InMemoryDbAsyncEnumerator<TEntity>(query.GetEnumerator())); 
    ((IQueryable<TEntity>)dbset).Provider.Returns(query.Provider); 
#endif 

    ... 

    dbset.Remove(Arg.Do<TEntity>(entity => 
           { 
            data.Remove(entity); 
            dbset.SetupData(data, find); 
           })); 

    ... 

    dbset.Add(Arg.Do<TEntity>(entity => 
           { 
           data.Add(entity); 
           dbset.SetupData(data, find); 
           }); 

    ... 

    return dbset; 
    } 
} 

我創建了一個測試方法,如:

[TestClass] 
public class ManipulationTests 
{ 
    [TestMethod] 
    public void Can_remove_set() 
    { 
    var blog = new Blog(); 
    var data = new List<Blog> { blog }; 

    var set = Substitute.For<DbSet<Blog>, IQueryable<Blog>, IDbAsyncEnumerable<Blog>>() 
         .SetupData(data); 

    set.Remove(blog); 

    var result = set.ToList(); 

    Assert.AreEqual(0, result.Count); 
    } 
} 

public class Blog 
{ 
    ... 
} 

當測試方法調用set.Remove(blog)的問題就出現了。它拋出一個InvalidOperationException帶有錯誤信息

收集被修改;枚舉操作可能不會執行。

這是因爲,當set.Remove(blog)方法被稱爲假data對象已被修改。但是,原來斯科特使用Moq的方式並不會導致問題。

因此,我包裹set.Remove(blog)方法與try ... catch (InvalidOperationException ex)塊,讓catch塊什麼都不做,那麼測試不拋出(當然)一個例外,如預期那樣獲得通過。

我知道這不是解決方案,但我怎樣才能實現我的目標,以單元測試DbSet<T>.Add()DbSet<T>.Remove()方法?

回答

1

這裏發生了什麼?

  1. set.Remove(blog); - 這將調用先前配置的lambda。
  2. data.Remove(entity); - 該項目從列表中刪除。
  3. dbset.SetupData(data, find); - 我們再次調用SetupData,用新列表重新配置Substitute。
  4. SetupData運行...
  5. 在那裏,正在調用dbSetup.Remove,以便重新配置下次調用Remove時發生的情況。

好的,我們在這裏有一個問題。 dtSetup.Remove(Arg.Do<T....不會重新配置任何東西,而是增加一個行爲,當您調用Remove時,應該發生的事情的替代者的內部列表。所以我們當前正在運行先前配置的Remove操作(1),同時,在堆棧下方,我們將一個操作添加到列表(5)。當堆棧返回並且迭代器查找要調用的下一個動作時,底層的模擬動作列表已更改。迭代器不喜歡變化。

由此得出結論:我們無法修改替代者在其模擬操作之一正在運行時所做的操作。如果你仔細想想,沒有人會讀到你的測試結果會發生,所以你不應該這樣做。

我們該如何解決?

public static DbSet<TEntity> SetupData<TEntity>(
    this DbSet<TEntity> dbset, 
    ICollection<TEntity> data = null, 
    Func<object[], TEntity> find = null) where TEntity : class 
{ 
    data = data ?? new List<TEntity>(); 
    find = find ?? (o => null); 

    Func<IQueryable<TEntity>> getQuery =() => new InMemoryAsyncQueryable<TEntity>(data.AsQueryable()); 

    ((IQueryable<TEntity>) dbset).Provider.Returns(info => getQuery().Provider); 
    ((IQueryable<TEntity>) dbset).Expression.Returns(info => getQuery().Expression); 
    ((IQueryable<TEntity>) dbset).ElementType.Returns(info => getQuery().ElementType); 
    ((IQueryable<TEntity>) dbset).GetEnumerator().Returns(info => getQuery().GetEnumerator()); 

#if !NET40 
    ((IDbAsyncEnumerable<TEntity>) dbset).GetAsyncEnumerator() 
              .Returns(info => new InMemoryDbAsyncEnumerator<TEntity>(getQuery().GetEnumerator())); 
    ((IQueryable<TEntity>) dbset).Provider.Returns(info => getQuery().Provider); 
#endif 

    dbset.Remove(Arg.Do<TEntity>(entity => data.Remove(entity))); 
    dbset.Add(Arg.Do<TEntity>(entity => data.Add(entity))); 

    return dbset; 
} 
  1. getQuery拉姆達創建一個新的查詢。它總是使用捕獲的列表data
  2. 所有.Returns配置調用使用lambda。在那裏,我們創建一個新的查詢實例,並在那裏委託我們的呼叫。
  3. RemoveAdd只修改我們捕獲的列表。我們不必重新配置我們的替代品,因爲每個調用都使用lambda表達式重新評估查詢。

儘管我非常喜歡NSubstitute,但我強烈建議您調查Effort, the Entity Framework Unit Testing Tool

你會使用這樣的:

// DbContext needs additional constructor: 
public class MyDbContext : DbContext 
{ 
    public MyDbContext(DbConnection connection) 
     : base(connection, true) 
    { 
    } 
} 

// Usage: 
DbConnection connection = Effort.DbConnectionFactory.CreateTransient();  
MyDbContext context = new MyDbContext(connection); 

有你有一個實際的DbContext,你可以用一切,實體框架給你,包括遷移,使用快速的內存數據庫使用。

+0

@@ Frank Ahh,對不起。我剛從假期回來。 :-)它就像一個魅力!我應該更多地瞭解lambda表達式。謝謝! – JustInChronicles 2014-12-29 04:27:52

相關問題