使用 DbSet 和 IQueryable 使用 NSubstitute 操作对象将返回错误

本文关键字:使用 错误 返回 对象 IQueryable DbSet NSubstitute 操作 | 更新日期: 2023-09-27 18:33:20

我想使用 NSubstitute 通过模拟 DbSet 来对实体框架 6.x 进行单元测试。幸运的是,Scott Xu 提供了一个很好的单元测试库,即使用 Moq 的 EntityFramework.Testing.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,错误消息为

集合已修改;枚举操作可能无法执行。

这是因为在调用 set.Remove(blog) 方法时,假data对象已被修改。但是,原始斯科特使用Moq的方式不会导致问题。

因此,我用try ... catch (InvalidOperationException ex)块包装了set.Remove(blog)方法,让catch块什么都不做,然后测试不会抛出异常(当然(,并且确实按预期通过。

我知道这不是解决方案,但是我如何实现单元测试DbSet<T>.Add()DbSet<T>.Remove()方法的目标?

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

这里发生了什么?

  1. set.Remove(blog); - 这将调用以前配置的 lambda。
  2. data.Remove(entity); - 从列表中删除该项目。
  3. dbset.SetupData(data, find); - 我们再次调用 SetupData,以使用新列表重新配置替换。
  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 lambda 创建一个新查询。它始终使用捕获的列表data
  2. 所有.Returns配置调用都使用 lambda。在那里,我们创建一个新的查询实例并在那里委托我们的调用。
  3. RemoveAdd只修改我们捕获的列表。我们不必重新配置我们的替代品,因为每次调用都会使用 lambda 表达式重新计算查询。

虽然我真的很喜欢 NSubstitute,但我强烈建议我研究实体框架单元测试工具 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,您可以使用快速内存数据库与实体框架为您提供的所有内容(包括迁移(一起使用。