操控与DbSet&LT对象; T>和IQueryable的< T>与NSubstitute返回错误 [英] Manipulating objects with DbSet<T> and IQueryable<T> with NSubstitute returns error

查看:359
本文介绍了操控与DbSet&LT对象; T>和IQueryable的< T>与NSubstitute返回错误的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想用嘲讽的 NSubstitute 单元测试实体框架6.x的= http://msdn.microsoft.com/en-us/library/gg696460(v=vs.113).aspx相对=nofollow> DbSet 。幸运的是,斯科特徐提供了良好的单元测试库的 EntityFramework.Testing.Moq 使用起订量。所以,我修改了代码,适用于NSubstitute和它已经看起来不错,到目前为止,直到我想测试 DbSet< T>。新增() DbSet< T>一个.remove()方法。这里是我的代码位:

 公共静态类NSubstituteDbSetExtensions 
{
公共静态DbSet< TEntity> SetupData< TEntity>(这DbSet< TEntity> dbset,ICollection的< TEntity>数据= NULL,Func键<对象[],TEntity>找到= NULL)其中TEntity:类
{
数据=数据?新的List< TEntity>();
查找=找到? (O => NULL);

变种查询=新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()返回(query.GetEnumerator());

#如果NET40
!((IDbAsyncEnumerable< TEntity>)dbset).GetAsyncEnumerator()返回(新InMemoryDbAsyncEnumerator< TEntity>(query.GetEnumerator()));
((IQueryable的< TEntity>)dbset).Provider.Returns(query.Provider);
#ENDIF

...

dbset.Remove(Arg.Do< TEntity>(实体=>
{
数据卸下摆臂(实体);
dbset.SetupData(数据,发现);
}));

...

dbset.Add(Arg.Do< TEntity>(实体=>
{
data.Add(实体);
dbset.SetupData(数据,发现);
});



返回dbset;
}
}

和我创建像一个测试方法:

  [TestClass中] 
公共类ManipulationTests
{
[TestMethod的]
公共无效Can_remove_set()
{
变种博客=新的博客();
VAR数据=新的List<博客> {博客};

VAR集= Substitute.For< DbSet<博客>中的IQueryable<博客> ;,IDbAsyncEnumerable<博客>>()
.SetupData(数据);

set.Remove(博客);

VAR的结果= set.ToList( );

Assert.AreEqual(0,result.Count);
}
}

公共类博客
{
...
}

产生的问题是,当测试方法调用 set.Remove(博客)。它抛出一个出现InvalidOperationException



错误信息

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




这是因为假数据对象当 set.Remove(博客)方法被调用了修改。然而,使用起订量原斯科特的方式不会导致的问题。



所以,我包裹 set.Remove(博客)方法以的try ... catch(InvalidOperationException异常前)块,并让块什么都不做,那么测试不抛出异常(当然),并如预期那样获得通过。



我知道这是不是解决办法,但如何能实现我的目标是单元测试 DbSet< T>。新增() DbSet< T>卸下摆臂()的方法呢?


解决方案

这里发生了什么?




  1. set.Remove(博客); - 这个调用先前配置的λ

  2. data.Remove(实体); - 该项目从列表中删除

  3. dbset .SetupData(数据,发现); - 我们再次呼吁SetupData,重新配置与新清单替代

  4. SetupData 运行...

  5. 在那里, dbSetup.Remove 被调用,以重新配置时会发生什么除去被称为下一次。



好吧,我们这里有一个问题。 dtSetup.Remove(Arg.Do<吨.... 没有的重新配置的东西,而它的补充的一行为替代内部的事情列表中应该发生,当你调用删除。因此,我们目前正在运行以前配置的删除操作(1),并在同一时间,下栈,我们要添加一个动作到列表( 5)当堆栈回报和迭代器查找下一个动作叫,嘲弄行动的基础列表已经改变。迭代器不喜欢改变。



这导致这样的结论:我们不能修改,而其戏弄的一个动作被运行。如果你仔细想想,没有人谁读你的测试会认为这种事情发生代课做什么,所以你不应该这样做,在所有。



我们怎样才能解决这个问题。

 公共静态DbSet< TEntity> ; SetupData< TEntity>(
本DbSet< TEntity> dbset,
的ICollection< TEntity>数据= NULL,
Func键<对象[],TEntity>找到= NULL)其中TEntity:类
{
数据=数据?新的List< TEntity>();
查找=找到? (O => NULL);

Func键<&IQueryable的LT; TEntity>> getQuery =()=>新InMemoryAsyncQueryable&所述; TEntity>(data.AsQueryable());

((IQueryable的< TEntity>)dbset).Provider.Returns(资讯= GT; getQuery()提供。);
((IQueryable的< TEntity>)dbset).Expression.Returns(资讯= GT; getQuery()表达。);
((IQueryable的< TEntity>)dbset).ElementType.Returns(资讯= GT; getQuery()的ElementType);
((IQueryable的< TEntity>)dbset).GetEnumerator()返回。(信息=方式> getQuery()的GetEnumerator());

#如果NET40
!((IDbAsyncEnumerable< TEntity>)dbset).GetAsyncEnumerator()
.Returns(资讯=>新建InMemoryDbAsyncEnumerator< TEntity>(getQuery()。的GetEnumerator()));
((IQueryable的< TEntity>)dbset).Provider.Returns(资讯= GT; getQuery()提供。);
#ENDIF

dbset.Remove(Arg.Do< TEntity>(实体=> data.Remove(实体)));
dbset.Add(Arg.Do< TEntity>(实体=> data.Add(实体)));

返回dbset;
}




  1. getQuery 的lambda创建一个新的查询。它总是使用捕捉列表数据

  2. 所有 .Returns 配置调用使用lambda。在那里,我们将创建一个新的查询实例,并有委托我们的电话。

  3. 删除添加只修改我们的俘虏名单。我们没有重新配置我们的替代者,因为每次调用重新评估使用lambda表达式查询。



虽然我真的很喜欢NSubstitute,我强烈建议寻找到 努力,实体框架单元测试工具



您会使用这样的:

  //的DbContext需要额外的构造函数: 
公共类MyDbContext:的DbContext
{
公共MyDbContext(的DbConnection连接)
:基地(连接,真)
{
}
}

//用法:
的DbConnection连接= Effort.DbConnectionFactory.CreateTransient();
MyDbContext背景=新MyDbContext(连接);

和你有一个实际的DbContext,您可以使用一切,实体框架给你,包括迁移使用,使用快速内存​​的数据库。


I'd like to use NSubstitute to unit test Entity Framework 6.x by mocking DbSet. Fortunately, Scott Xu provides a good unit testing library, EntityFramework.Testing.Moq using 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. 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). It throws an InvalidOperationException with error message of

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. However, the original Scott's way using Moq doesn't result in the issue.

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.

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?

解决方案

What's happening here?

  1. set.Remove(blog); - this calls the previously configured 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.
  4. SetupData runs...
  5. In there, dbSetup.Remove is being called, in order to reconfigure what happens when Remove is called next time.

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.

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. It always uses the captured list data.
  2. All .Returns configuration calls use a lambda. In there, we create a new query instance and delegate our call there.
  3. Remove and Add only modify our captured list. We don't have to reconfigure our Substitute, because every call reevaluates the query using the lambda expressions.

While I really like NSubstitute, I would strongly recommend looking into Effort, the Entity Framework Unit Testing Tool.

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.

这篇关于操控与DbSet&LT对象; T&GT;和IQueryable的&LT; T&GT;与NSubstitute返回错误的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆