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

时间:2014-12-05 03:21:23

标签: c# entity-framework unit-testing nsubstitute dbset

我想使用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,错误消息为

  

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

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

因此,我将set.Remove(blog)方法包装为try ... catch (InvalidOperationException ex)块并让catch块不执行任何操作,然后测试不会抛出异常(当然)并且确实得到了按预期通过。

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

1 个答案:

答案 0 :(得分:2)

这里发生了什么?

  1. set.Remove(blog); - 这会调用先前配置的lambda。
  2. data.Remove(entity); - 该项目已从列表中删除。
  3. dbset.SetupData(data, find); - 我们再次调用SetupData,使用新列表重新配置替换。
  4. SetupData运行...
  5. 在那里,正在调用dbSetup.Remove,以便重新配置下次调用Remove时会发生什么。
  6. 好的,我们这里有问题。 dtSetup.Remove(Arg.Do<T....没有重新配置任何内容,而是将行为添加到替换的内部列表中,当您调用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表达式重新评估查询。
    4. 虽然我非常喜欢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,您可以使用Entity Framework为您提供的所有内容,包括迁移,使用快速的内存数据库。