简体   繁体   English

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

[英]Manipulating objects with DbSet<T> and IQueryable<T> with NSubstitute returns error

I'd like to use NSubstitute to unit test Entity Framework 6.x by mocking DbSet . 我想使用NSubstitute被嘲讽单元测试实体框架6.x的DbSet Fortunately, Scott Xu provides a good unit testing library, EntityFramework.Testing.Moq using Moq . 幸运的是, Scott Xu使用Moq提供了一个很好的单元测试库EntityFramework.Testing.Moq So, I modified his code to be suitable for NSubstitute and it's been looking good so far, until I wanted to test DbSet<T>.Add() , DbSet<T>.Remove() methods. 所以,我修改了他的代码以适合NSubstitute,到目前为止它一直很好看,直到我想测试DbSet<T>.Add()DbSet<T>.Remove()方法。 Here's my code bits: 这是我的代码位:

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;
  }
}

And I created a test method like: 我创建了一个测试方法,如:

[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
{
   ...
}

The issue arises when the test method calls set.Remove(blog) . 当测试方法调用set.Remove(blog)时会出现问题。 It throws an InvalidOperationException with error message of 它抛出一个InvalidOperationException ,错误消息为

Collection was modified; 收集被修改; enumeration operation may not execute. 枚举操作可能无法执行。

This is because the fake data object has been modified when the set.Remove(blog) method is called. 这是因为在调用set.Remove(blog)方法时修改了伪data对象。 However, the original Scott's way using Moq doesn't result in the issue. 然而,原始Scott使用Moq的方式不会导致问题。

Therefore, I wrapped the set.Remove(blog) method with a try ... catch (InvalidOperationException ex) block and let the catch block do nothing, then the test doesn't throw an exception (of course) and does get passed as expected. 因此,我用一个try ... catch (InvalidOperationException ex)块包装了set.Remove(blog)方法,让catch块什么都不做,然后测试不会抛出异常(当然)并且确实传递为预期。

I know this is not the solution, but how can I achieve my goal to unit test DbSet<T>.Add() and DbSet<T>.Remove() methods? 我知道这不是解决方案,但是如何实现单元测试DbSet<T>.Add()DbSet<T>.Remove()方法的目标?

What's happening here? 这里发生了什么事?

  1. set.Remove(blog); - this calls the previously configured lambda. - 这会调用先前配置的lambda。
  2. data.Remove(entity); - The item is removed from the list. - 该项目已从列表中删除。
  3. dbset.SetupData(data, find); - We call SetupData again, to reconfigure the Substitute with the new list. - 我们再次调用SetupData,用新列表重新配置替换。
  4. SetupData runs... SetupData运行...
  5. In there, dbSetup.Remove is being called, in order to reconfigure what happens when Remove is called next time. 在那里,正在调用dbSetup.Remove ,以便重新配置下次调用Remove时会发生什么。

Okay, we have a problem here. 好的,我们这里有问题。 dtSetup.Remove(Arg.Do<T.... doesn't reconfigure anything, it rather adds a behavior to the Substitute's internal list of things that should happen when you call Remove. So we're currently running the previously configured Remove action (1) and at the same time, down the stack, we're adding an action to the list (5). When the stack returns and the iterator looks for the next action to call, the underlying list of mocked actions has changed. Iterators don't like changes. dtSetup.Remove(Arg.Do<T....重新配置任何东西,而是在Substitute的内部列表中添加一个行为,当你调用Remove时会发生这种情况。所以我们当前正在运行先前配置的Remove动作(1)同时,在堆栈中,我们向列表添加一个动作(5)。当堆栈返回并且迭代器查找下一个要调用的动作时,模拟动作的基础列表已经改变。迭代器不喜欢变化。

This leads to the conclusion: We can't modify what a Substitute does while one of its mocked actions is running. 这导致了结论:当我们的一个模拟动作正在运行时,我们无法修改替换所做的事情。 If you think about it, nobody who reads your test would assume this to happen, so you shouldn't do this at all. 如果你考虑一下,没有人读你的测试会认为这发生了,所以你根本不应该这样做。

How can we fix it? 我们该如何解决?

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. The getQuery lambda creates a new query. getQuery lambda创建一个新查询。 It always uses the captured list data . 它始终使用捕获的列表data
  2. All .Returns configuration calls use a lambda. 所有.Returns配置调用都使用lambda。 In there, we create a new query instance and delegate our call there. 在那里,我们创建一个新的查询实例并在那里委托我们的调用。
  3. Remove and Add only modify our captured list. RemoveAdd仅修改我们捕获的列表。 We don't have to reconfigure our Substitute, because every call reevaluates the query using the lambda expressions. 我们不必重新配置我们的替换,因为每个调用都使用lambda表达式重新评估查询。

While I really like NSubstitute, I would strongly recommend looking into Effort, the Entity Framework Unit Testing Tool . 虽然我非常喜欢NSubstitute,但我强烈建议您查看实体框架单元测试工具Effort

You would use it like this: 你会像这样使用它:

// 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);

And there you have an actual DbContext that you can use with everything that Entity Framework gives you, including migrations, using a fast in-memory-database. 并且您有一个实际的DbContext,您可以使用Entity Framework为您提供的所有内容,包括迁移,使用快速的内存数据库。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM