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

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

问题描述

我目前正在 MVC4 应用程序中为我的存储库实现编写单元测试.为了模拟数据上下文,我开始采用来自 这篇文章,但我现在发现了一些限制,让我怀疑是否可以正确模拟 IQueryable.

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.

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

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

在我的测试环境中,我尝试使用上面提到的假 DbSet 实现来模拟 posts,并且还通过创建 ListListcode>Post 实例并使用 AsQueryable() 扩展方法将其转换为 IQueryable.这两种方法都在测试条件下工作,但代码在生产中实际上失败了,以下例外:

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:无法创建类别"类型的常量值.在此上下文中仅支持原始类型或枚举类型.

尽管像这样的 LINQ 问题很容易解决,但真正的挑战是找到它们,因为它们不会在测试环境中暴露出来.

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.

我期望我可以模拟实体框架的 IQueryable 实现的行为是不切实际的吗?

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

感谢您的想法,

蒂姆.

推荐答案

我认为模拟实体框架的行为是非常困难的,如果不可能的话.首先也是最重要的,因为它需要对 linq-to-entites 与 linq-to-objects 不同的所有特性和边缘情况有深入的了解.正如您所说:真正的挑战是找到它们.让我指出三个主要领域,但并不声称几乎详尽无遗:

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:

Linq-to-Objects 成功而 Linq-to-Entities 失败的情况:

  • .Select(x => x.Property1.ToString().LINQ to Entities 无法识别方法System.String ToString()"方法... 这适用于本地 .Net 类中的几乎所有方法,当然也适用于自己的方法.只有少数 .Net 方法会被转换为 SQL.请参阅 CLR 方法到规范函数映射.从 EF 6.1 开始,支持 ToString 的方式.但只有无参数重载.
  • Skip() 没有前面的 OrderBy.
  • ExceptIntersect:可以产生可怕的查询,抛出SQL 语句的某些部分嵌套太深.重写查询或将其分解为更小的查询.
  • Select(x => x.Date1 - x.Date2):DbArithmeticExpression 参数必须具有数字通用类型.
  • (您的情况).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 } : new B { BProp1= x.Prop1 }) }):类型B"出现在单个 LINQ to Entities 查询中的两个结构不兼容的初始化中... 来自 此处.
  • context.Entities.Cast():无法将实体"类型转换为IEntity"类型.LINQ to Entities 仅支持转换 EDM 原语或枚举类型.
  • .Select(p => p.Category?.Name).在表达式中使用空传播会引发 CS8072 表达式树 lambda 可能不包含空传播运算符. 这个 可能有一天会修复.
  • 这个问题:为什么 Select、Where 和 GroupBy 的这种组合会导致异常? 让我意识到这个事实甚至有 EF 不支持的整个查询结构,而 L2O 不会有任何问题.
  • .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.

Linq-to-Objects 失败而 Linq-to-Entities 成功的情况:

  • .Select(p => p.Category.Name):当 p.Category 为空时 L2E 返回空,但 L2O 抛出 对象引用不设置为对象的实例.这不能通过使用空传播来解决(见上文).
  • Nodes.Max(n => n.ParentId.Value) 带有一些 n.ParentId 的空值.L2E 返回一个最大值,L2O 抛出 Nullable 对象必须有一个值.
  • 使用 EntityFunctions(DbFunctions 自 EF 6)或 SqlFunctions.
  • .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.

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

  • Nodes.Include("ParentNodes"):L2O 没有包含的实现.它将运行并返回节点(如果 NodesIQueryable),但没有父节点.
  • Nodes.Select(n => n.ParentNodes.Max(p => p.Id)) 带有一些空的 ParentNodes 集合:都失败了,但有不同的例外.
  • Nodes.Where(n => n.Name.Contains("par")):L2O 区分大小写,L2E 取决于数据库整理(通常不区分大小写).莉>
  • node.ParentNode = parentNode:具有双向关系,在 L2E 中,这也会将节点添加到父节点的节点集合中(关系修复).不在 L2O 中.(请参阅对双向 EF 关系进行单元测试).
  • 空传播失败的解决方法:.Select(p => p.Category == null ? string.Empty : p.Category.Name):结果是一样的,但是生成的 SQL 查询还包含空检查,可能更难优化.
  • Nodes.AsNoTracking().Select(n => n.ParentNode.这个非常棘手!.With AsNoTracking EF 为每个 Node 创建新的 ParentNode 对象,所以可能会有重复.Without AsNoTracking> EF 重用现有的ParentNodes,因为现在涉及到实体状态管理器和实体键.AsNoTracking() 可以在L2O 中调用,但它什么也不做,所以有或没有它永远不会有区别.
  • 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.

那么模拟延迟加载/急切加载以及上下文生命周期对延迟加载异常的影响呢?或者某些查询构造对性能的影响(例如触发 N+1 SQL 查询的构造).或者由于重复或丢失实体键而导致的异常?还是关系修复?

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?

我的意见:没有人会伪造它.最令人担忧的领域是 L2O 成功和 L2E 失败的地方.现在绿色单元测试的价值是什么?之前有人说过,EF 只能在集成测试中可靠地进行测试(例如此处),我倾向于同意.

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.

然而,这并不意味着我们应该忘记以 EF 作为数据层的项目中的单元测试.有方法,但是,我认为,并非没有集成测试.

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天全站免登陆