如何模拟EntityFramework实现IQueryable的局限性 [英] How to mock the limitations of EntityFramework's implementation of IQueryable

查看:167
本文介绍了如何模拟EntityFramework实现IQueryable的局限性的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在为MVC4应用程序中的存储库实现编写单元测试。为了模拟数据环境,我开始采取一些想法从这篇文章,但是我现在已经发现了一些限制,使我怀疑是否有可能正确地模拟 IQueryable



特别是,我看到一些情况,测试通过,但代码在生产中失败,我还没有找到任何方式来模拟行为这导致这种失败。



例如,以下代码片段用于选择落入预定义的 Post 实体类别列表:

  var posts = repository.GetEntities< Post>(); //返回IQueryable< Post> 
var categories = GetCategoriesInGroup(Post); //返回一个类型为
的固定列表var filtered = posts.Where(p => categories.Any(c => c.Name == p.Category))ToList();

在我的测试环境中,我试过嘲笑帖子使用上面提到的假的 DbSet 实现,并通过创建一个列表 Post 实例,并使用 AsQueryable()扩展方法将其转换为 IQueryable 。这两种方法在测试条件下工作,但代码实际上在生产中失败,但有以下例外:




System.NotSupportedException :无法创建类型类别的常量值。在此上下文中仅支持基本类型或枚举类型。



尽管这样的LINQ问题很容易解决,真正的挑战是找到它们,因为他们没有透露自己在测试环境中。



我是不切实际的,期望我可以模拟Entity Framework执行 IQueryable



感谢您的想法,



Tim。

解决方案

我认为如果不可能,模拟实体框架行为是非常非常困难的。首先,最重要的是因为需要对linq-to-entites与linq-to-object不同的所有特征和边缘情况的深刻了解。正如你所说:真正的挑战是找到他们。让我指出三个主要领域,而不用说是不完整的:



Linq对对象成功和Linq到实体失败的情况: / strong>




  • .Select(x => x.Property1.ToString()。 LINQ to Entities不能识别方法'System.String ToString()'方法... 这适用于几乎所有的native .NET类方法,当然也适用于自己的方法,只有几个.Net方法将被翻译成SQL。请参阅 CLR方法到规范功能映射。从EF 6.1开始,支持 ToString ,但只有无参数的重载。

  • Skip()没有前面的 OrderBy

  • 相交:可以产生throw produce produce rous rous throw throw throw throw。。。。。。。。。。。。。。。 k />
  • 选择(x =>

  • (你的情况) .Where(p => p.Category == category)在此上下文中仅支持原始类型或枚举类型。

  • Nodes.Where(n => n.ParentNodes.First()。Id == 1)方法'First'只能用作最终查询操作。

  • context.Nodes.Last() LINQ to Entities不认识方法'... Last ...'。这适用于许多其他 IQueryable 扩展方法。请参阅支持和不支持的LINQ方法

  • (见下面的Slauma的评论): .Select(x => new A {Property1 =(x.BoolProperty?new B {BProp1 = x.Prop1,BProp2 = x.Prop2}新的B {BProp1 = x.Prop1})})类型B出现在单个LINQ to Entities查询中的两个结构不兼容的初始化中... a href =https://stackoverflow.com/q/10904375/861716>这里。

  • context.Entities.Cast< IEntity> ()无法将类型Entity转换为键入IEntity。 LINQ to Entities只支持投射EDM原始或枚举类型。

  • .Select(p => p.Category? C $ C>。在表达式中使用空传播throws CS8072表达式树lambda可能不包含空传播运算符。 可能会在一天内被修复

  • 此问题:为什么Select,Where和GroupBy导致例外?的组合使我意识到事实即使是完整的查询结构,EF不支持,而L2O也不会有任何麻烦。



Linq到对象失败的情况,Linq-to-Entities成功:




  • 。选择(p => p.Category.Name):当 p.Category 为空时L2E返回null,但L2O抛出对象引用未设置为对象的实例。无法修复y使用空传播(见上文)。

  • Nodes.Max(n => n.ParentId.Value) n.ParentId 有一些空值。使用 EntityFunctions DbFunctions 从EF 6)或 SqlFunctions



成功/失败但行为不同的情况:




  • Nodes.Include(ParentNodes):L2O没有include的实现。它将运行并返回节点(如果节点 IQueryable ),但没有父节点。

  • Nodes.Select(n => n.ParentNodes.Max(p => p.Id))与一些空的父节点集合:都失败但有不同的例外。

  • Nodes.Where(n => n.Name.Contains )):L2O区分大小写,L2E取决于数据库归类(通常不区分大小写)。

  • node.ParentNode = parentNode :具有双向关系,在L2E中,这也将节点添加到父节点集合( relationship fixup )。不在L2O (见单元测试双向EF关系)。

  • 解决失败的空传播的方法: .Select(p => p.Category == null?string.Empty:p.Category.Name):结果是一样的,但生成的SQL查询也包含空检查,可能难以优化。

  • Nodes.AsNoTracking()。选择(n => n.ParentNode 。这是一个非常棘手的 AsNoTracking EF为每个 Node 创建新的 ParentNode 对象,所以可以是重复的。 em> AsNoTracking EF重复使用现有的 ParentNodes ,因为现在涉及实体状态管理器和实体密钥。 AsNoTracking()可以在L2O中调用,但它不做任何事情,所以永远不会有差异。



而且mockin怎么样? g懒惰/渴望加载和上下文生命周期对延迟加载异常的影响?或一些查询结构对性能的影响(如触发N + 1 SQL查询的结构)。或由于重复或缺少实体密钥引起的异常?还是关系解决?



我的意见:没有人会假的。最令人震惊的地方是L2O成功,L2E失败。现在绿色单元测试有什么价值?之前已经说过,EF只能在集成测试中可靠地进行测试(例如此处),我倾向于同意。 / p>

但是,这并不意味着我们应该忘记使用EF作为数据层的项目中的单元测试。有如何做到这一点,但是,我认为,不是没有集成测试。


I am currently writing unit tests for my repository implementation in an MVC4 application. In order to mock the data context, I started by adopting some ideas from this post, but I have now discovered some limitations that make me question whether it is even possible to properly mock IQueryable.

In particular, I have seen some situations where the tests pass but the code fails in production and I have not been able to find any way to mock the behavior that causes this failure.

For example, the following snippet is used to select Post entities that fall within a predefined list of categories:

var posts = repository.GetEntities<Post>(); // Returns IQueryable<Post>
var categories = GetCategoriesInGroup("Post"); // Returns a fixed list of type Category
var filtered = posts.Where(p => categories.Any(c => c.Name == p.Category)).ToList();

In my test environment, I have tried mocking posts using the fake DbSet implementation mentioned above, and also by creating a List of Post instances and converting it to IQueryable using the AsQueryable() extension method. Both of these approaches work under test conditions, but the code actually fails in production, with the following exception:

System.NotSupportedException : Unable to create a constant value of type 'Category'. Only primitive types or enumeration types are supported in this context.

Although LINQ issues like this are easy enough to fix, the real challenge is finding them, given that they do not reveal themselves in the test environment.

Am I being unrealistic in expecting that I can mock the behavior of Entity Framework's implementation of IQueryable?

Thanks for your ideas,

Tim.

解决方案

I think it is very very hard, if impossible, to mock Entity Framework behaviour. First and foremost because it would require profound knowledge of all peculiarities and edge cases where linq-to-entites differs from linq-to-objects. As you say: the real challenge is finding them. Let me point out three main areas without claiming to be even nearly exhaustive:

Cases where Linq-to-Objects succeeds and Linq-to-Entities fails:

  • .Select(x => x.Property1.ToString(). LINQ to Entities does not recognize the method 'System.String ToString()' method... This applies to nearly all methods in native .Net classes and of course to own methods. Only a few .Net methods will be translated into SQL. See CLR Method to Canonical Function Mapping. As of EF 6.1, ToString is supported by the way. But only the parameterless overload.
  • Skip() without preceding OrderBy.
  • Except and Intersect: can produce monstrous queries that throw Some part of your SQL statement is nested too deeply. Rewrite the query or break it up into smaller queries.
  • Select(x => x.Date1 - x.Date2): DbArithmeticExpression arguments must have a numeric common type.
  • (your case) .Where(p => p.Category == category): Only primitive types or enumeration types are supported in this context.
  • Nodes.Where(n => n.ParentNodes.First().Id == 1): The method 'First' can only be used as a final query operation.
  • context.Nodes.Last(): LINQ to Entities does not recognize the method '...Last...'. This applies to many other IQueryable extension methods. See Supported and Unsupported LINQ Methods.
  • (See Slauma's comment below): .Select(x => new A { Property1 = (x.BoolProperty ? new B { BProp1 = x.Prop1, BProp2 = x.Prop2 } : new B { BProp1 = x.Prop1 }) }): The type 'B' appears in two structurally incompatible initializations within a single LINQ to Entities query... from here.
  • context.Entities.Cast<IEntity>(): Unable to cast the type 'Entity' to type 'IEntity'. LINQ to Entities only supports casting EDM primitive or enumeration types.
  • .Select(p => p.Category?.Name). Using null propagation in an expression throws CS8072 An expression tree lambda may not contain a null propagating operator. This may get fixed one day.
  • This question: Why does this combination of Select, Where and GroupBy cause an exception? made me aware of the fact that there are even entire query constructions that are not supported by EF, while L2O wouldn't have any trouble with them.

Cases where Linq-to-Objects fails and Linq-to-Entities succeeds:

  • .Select(p => p.Category.Name): when p.Category is null L2E returns null, but L2O throws Object reference not set to an instance of an object. This can't be fixed by using null propagation (see above).
  • Nodes.Max(n => n.ParentId.Value) with some null values for n.ParentId. L2E returns a max value, L2O throws Nullable object must have a value.
  • Using EntityFunctions (DbFunctions as of EF 6) or SqlFunctions.

Cases where both succeed/fail but behave differently:

  • Nodes.Include("ParentNodes"): L2O has no implementation of include. It will run and return nodes (if Nodes is IQueryable), but without parent nodes.
  • Nodes.Select(n => n.ParentNodes.Max(p => p.Id)) with some empty ParentNodes collections: both fail but with different exceptions.
  • Nodes.Where(n => n.Name.Contains("par")): L2O is case sensitive, L2E depends on the database collation (often not case sensitive).
  • node.ParentNode = parentNode: with a bidirectional relationship, in L2E this will also add the node to the nodes collection of the parent (relationship fixup). Not in L2O. (See Unit testing a two way EF relationship).
  • Work-around for failing null propagation: .Select(p => p.Category == null ? string.Empty : p.Category.Name): the result is the same, but the generated SQL query also contains the null check and may be harder to optimize.
  • Nodes.AsNoTracking().Select(n => n.ParentNode. This one is very tricky!. With AsNoTracking EF creates new ParentNode objects for each Node, so there can be duplicates. Without AsNoTracking EF reuses existing ParentNodes, because now the entity state manager and entity keys are involved. AsNoTracking() can be called in L2O, but it doesn't do anything, so there will never be a difference with or without it.

And what about mocking lazy/eager loading and the effect of context life cycle on lazy loading exceptions? Or the effect of some query constructs on performance (like constructs that trigger N+1 SQL queries). Or exceptions due to duplicate or missing entity keys? Or relationship fixup?

My opinion: nobody is going to fake that. The most alarming area is where L2O succeeds and L2E fails. Now what's the value of green unit tests? It has been said before that EF can only reliably be tested in integration tests (e.g. here) and I tend to agree.

However, that does not mean that we should forget about unit tests in projects with EF as data layer. There are ways to do it, but, I think, not without integration tests.

这篇关于如何模拟EntityFramework实现IQueryable的局限性的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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