操控与DbSet<对象; T>和IQueryable的< T>与NSubstitute返回错误 [英] Manipulating objects with DbSet<T> and IQueryable<T> with NSubstitute returns error
问题描述
我想用嘲讽的 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>卸下摆臂()
的方法呢?
解决方案这里发生了什么?
set.Remove(博客);
- 这个调用先前配置的λ
data.Remove(实体);
- 该项目从列表中删除
dbset .SetupData(数据,发现);
- 我们再次呼吁SetupData,重新配置与新清单替代
SetupData
运行...
- 在那里,
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;
}
- 的
getQuery
的lambda创建一个新的查询。它总是使用捕捉列表数据
。
- 所有
.Returns
配置调用使用lambda。在那里,我们将创建一个新的查询实例,并有委托我们的电话。
删除
和添加
只修改我们的俘虏名单。我们没有重新配置我们的替代者,因为每次调用重新评估使用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 anInvalidOperationException
with error message ofCollection was modified; enumeration operation may not execute.
This is because the fake
data
object has been modified when theset.Remove(blog)
method is called. However, the original Scott's way usingMoq
doesn't result in the issue.Therefore, I wrapped the
set.Remove(blog)
method with atry ... catch (InvalidOperationException ex)
block and let thecatch
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()
andDbSet<T>.Remove()
methods?解决方案What's happening here?
set.Remove(blog);
- this calls the previously configured lambda.data.Remove(entity);
- The item is removed from the list.dbset.SetupData(data, find);
- We call SetupData again, to reconfigure the Substitute with the new list.SetupData
runs...- 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; }
- The
getQuery
lambda creates a new query. It always uses the captured listdata
.- All
.Returns
configuration calls use a lambda. In there, we create a new query instance and delegate our call there.Remove
andAdd
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<对象; T>和IQueryable的< T>与NSubstitute返回错误的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!