使用带有NSubstitute的DbSet 和IQueryable 操作对象会返回错误

我想使用NSubstitute通过模拟DbSet对Entity Framework 6.x进行unit testing。 幸运的是, Scott Xu使用Moq提供了一个很好的unit testing库EntityFramework.Testing.Moq 。 因此,我修改了他的代码以适合NSubstitute并且它一直看起来很好,直到我想测试DbSet.Add()DbSet.Remove()方法。 这是我的代码位:

 public static class NSubstituteDbSetExtensions { public static DbSet SetupData(this DbSet dbset, ICollection data = null, Func find = null) where TEntity : class { data = data ?? new List(); find = find ?? (o => null); var query = new InMemoryAsyncQueryable(data.AsQueryable()); ((IQueryable)dbset).Provider.Returns(query.Provider); ((IQueryable)dbset).Expression.Returns(query.Expression); ((IQueryable)dbset).ElementType.Returns(query.ElementType); ((IQueryable)dbset).GetEnumerator().Returns(query.GetEnumerator()); #if !NET40 ((IDbAsyncEnumerable)dbset).GetAsyncEnumerator().Returns(new InMemoryDbAsyncEnumerator(query.GetEnumerator())); ((IQueryable)dbset).Provider.Returns(query.Provider); #endif ... dbset.Remove(Arg.Do(entity => { data.Remove(entity); dbset.SetupData(data, find); })); ... dbset.Add(Arg.Do(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 }; var set = Substitute.For<DbSet, IQueryable, IDbAsyncEnumerable>() .SetupData(data); set.Remove(blog); var result = set.ToList(); Assert.AreEqual(0, result.Count); } } public class Blog { ... } 

当测试方法调用set.Remove(blog)时会出现问题。 它抛出一个InvalidOperationException ,错误消息为

collections被修改; 枚举操作可能无法执行。

这是因为在调用set.Remove(blog)方法时修改了伪data对象。 然而,原始Scott使用Moq的方式不会导致问题。

因此,我用一个try ... catch (InvalidOperationException ex)块包装了set.Remove(blog)方法,让catch块什么都不做,然后测试不会抛出exception(当然)并且确实传递为预期。

我知道这不是解决方案,但是如何实现unit testingDbSet.Add()DbSet.Remove()方法的目标?

这里发生了什么事?

  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重新配置任何内容,而是向Substitute的内部列表添加一个行为,当你调用Remove时会发生这种情况。所以我们当前正在运行先前配置的Remove动作(1)同时,在堆栈中,我们向列表添加一个动作(5)。当堆栈返回并且迭代器查找下一个要调用的动作时,模拟动作的基础列表已经改变。迭代器不喜欢变化。

这导致了结论:当我们的一个模拟动作正在运行时,我们无法修改替代品的作用。 如果你考虑一下,没有人读你的测试会认为这发生了,所以你根本不应该这样做。

我们该如何解决?

 public static DbSet SetupData( this DbSet dbset, ICollection data = null, Func find = null) where TEntity : class { data = data ?? new List(); find = find ?? (o => null); Func> getQuery = () => new InMemoryAsyncQueryable(data.AsQueryable()); ((IQueryable) dbset).Provider.Returns(info => getQuery().Provider); ((IQueryable) dbset).Expression.Returns(info => getQuery().Expression); ((IQueryable) dbset).ElementType.Returns(info => getQuery().ElementType); ((IQueryable) dbset).GetEnumerator().Returns(info => getQuery().GetEnumerator()); #if !NET40 ((IDbAsyncEnumerable) dbset).GetAsyncEnumerator() .Returns(info => new InMemoryDbAsyncEnumerator(getQuery().GetEnumerator())); ((IQueryable) dbset).Provider.Returns(info => getQuery().Provider); #endif dbset.Remove(Arg.Do(entity => data.Remove(entity))); dbset.Add(Arg.Do(entity => data.Add(entity))); return dbset; } 
  1. getQuery lambda创建一个新查询。 它始终使用捕获的列表data
  2. 所有.Returns配置调用都使用lambda。 在那里,我们创建一个新的查询实例并在那里委托我们的调用。
  3. RemoveAdd仅修改我们捕获的列表。 我们不必重新配置替换,因为每个调用都使用lambda表达式重新评估查询。

虽然我非常喜欢NSubstitute,但我强烈建议您查看entity frameworkunit testing工具Effort 。

你会像这样使用它:

 // 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,您可以使用Entity Framework为您提供的所有内容,包括迁移,使用快速的内存数据库。