使用 DbSet<T> 和 IQueryable<T> 操作对象,在 NSubstitute 中返回错误

3

我想使用NSubstitute通过模拟DbSet来对Entity Framework 6.x进行单元测试。幸运的是,Scott Xu提供了一个很好的单元测试库EntityFramework.Testing.Moq,使用Moq。因此,我修改了他的代码,以适用于NSubstitute,并且目前看起来效果不错,直到我想要测试DbSet<T>.Add()DbSet<T>.Remove()方法。以下是我的代码片段:

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,错误消息为:

Collection was modified; enumeration operation may not execute.

这是因为当调用set.Remove(blog)方法时,虚拟的data对象已被修改。然而,原始的Scott使用Moq的方式并不会出现这个问题。
因此,我用一个try ... catch (InvalidOperationException ex)块包装了set.Remove(blog)方法,并让catch块什么也不做,然后测试不会抛出异常(当然),并按预期通过。
我知道这不是解决方案,但我如何实现我的目标来单元测试DbSet<T>.Add()DbSet<T>.Remove()方法?
1个回答

2
这里发生了什么事情?
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....并没有“重新配置”任何内容,而实际上它是将一种行为添加到Substitute的内部列表中,当您调用Remove时,它会执行这些行为。因此,在执行(1)的同时,在堆栈下方,我们正在向列表中添加操作(5)。当堆栈返回并且迭代器查找要调用的下一个操作时,模拟操作的基础列表已更改。迭代器不喜欢变化。
这导致结论:当Substitute的模拟操作正在运行时,我们不能修改其所做的操作。如果你想一下,没有人会认为测试会出现这种情况,所以你根本不应该这么做。
我们该如何解决?
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 lambda会创建一个新的查询。它总是使用捕获的列表data
  2. 所有.Returns配置调用都使用lambda表达式。在其中,我们创建一个新的查询实例并将我们的调用委托给那里。
  3. RemoveAdd仅修改我们捕获的列表。我们不必重新配置Substitute,因为每个调用都使用lambda表达式重新评估查询。

虽然我真的很喜欢NSubstitute,但我强烈建议您了解Effort,Entity Framework单元测试工具

您可以像这样使用它:

// 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提供的一切一起使用,包括迁移,使用快速的内存数据库。


@@Frank 啊,抱歉。我刚从假期回来。:-) 它运行得非常好!我应该学习更多关于lambda表达式的知识。谢谢! - justinyoo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接