如何模拟EntityFramework的IQueryable实现的局限性

Tim*_*ter 28 entity-framework iqueryable mocking

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

特别是,我已经看到了一些测试通过但代码在生产中失败的情况,并且我无法找到任何方法来模拟导致此失败的行为.

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

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();
Run Code Online (Sandbox Code Playgroud)

在我的测试环境中,我试图嘲弄posts使用假DbSet上面提到的实施,同时也通过创建ListPost实例并将其转换为IQueryable使用AsQueryable()扩展方法.这两种方法都在测试条件下工作,但代码实际上在生产中失败,但有以下例外:

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

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

我期望我可以嘲笑实体框架的实施行为,这是不现实的IQueryable吗?

谢谢你的想法,

蒂姆.

Ger*_*old 63

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

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内实体查询......这里.
  • context.Entities.Cast<IEntity>():无法将类型'Entity'强制转换为'IEntity'.LINQ to Entities仅支持转换EDM原语或枚举类型.
  • .Select(p => p.Category?.Name).在表达式中使用空传播抛出CS8072表达式树lambda可能不包含空传播运算符.可能会在一天内修复.
  • 这个问题:为什么Select,Where和GroupBy的这种组合会导致异常?让我意识到甚至整个查询结构都不受EF支持,而L2O也不会有任何问题.

Linq-to-Objects失败并且Linq-to-Entities成功的案例:

  • .Select(p => p.Category.Name):when p.Category为null时,L2E返回null,但L2O抛出的Object引用未设置为对象的实例.这不能通过使用空传播来修复(参见上文).
  • Nodes.Max(n => n.ParentId.Value)带有一些空值n.ParentId.L2E返回一个最大值,L2O抛出Nullable对象必须有一个值.
  • 使用EntityFunctions(DbFunctions截至EF 6)或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.这个非常棘手!.使用 AsNoTracking EF ParentNode为每个创建新对象Node,因此可能存在重复项.没有 AsNoTracking EF重用现有的ParentNodes,因为现在涉及实体状态管理器和实体密钥.AsNoTracking()可以在L2O中调用,但它不会做任何事情,所以无论是否有它都不会有任何区别.

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

我的意见是:没有人打算这样做.最令人担忧的领域是L2O成功而L2E失败.现在绿色单元测试的价值是多少?之前已经说过EF只能在集成测试中可靠地进行测试(例如这里),我倾向于同意.

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

  • 哇!这是一个非常详细的答案,非常有启发性.这不是我想听到的,但它迫使我采取现实检查(并可能使我免于浪费大量时间).谢谢. (5认同)
  • 大!这是一个很棒的详细收藏!我必须喜欢这个作为LINQ!= LINQ的终极示例!对于第1节,我有另一个:`.选择(x =>新A {Property1 =(x.BoolProperty?new B {BProp1 = x.Prop1,BProp2 = x.Prop2}:new B {BProp1 = x.Prop1} }})`:*类型'B'出现在单个LINQ to Entities查询等中的两个结构上不兼容的初始化中.*(从这里:http://stackoverflow.com/questions/10904375/can-i-项目-AN-可选组基准的-AN-实体 - 进入 - 一个-可选组基准的-T) (3认同)
  • 更糟糕的是:在EF中有效的方法取决于所使用的数据库.我现在没有具体的例子,但我的查询在SQL Server 2000上失败了,但在2005 +上工作.可能还有一些查询适用于SQL Server,但在(例如)MySQL上失败. (2认同)
  • @ user89861我知道这个工具,但我担心使用它,因为它为测试环境添加了一个复杂的第三方工具,我不知道它是否总是正常运行.它基本上是基于文件的数据库的查询提供程序.甚至不同的RDMBS查询提供程序也有不同的行为!此外,如果您无法通过自己的应用程序前端执行此操作,请不要低估有意义的模拟数据和测试用例的维护. (2认同)