我目前正在为MVC4应用程序中的存储库实现编写单元测试。为了模拟数据上下文,我首先采用了this post的一些想法,但是现在我发现了一些局限性,这些疑问使我怀疑是否有可能正确模拟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();
在我的测试环境中,我尝试使用上述伪造的
posts
实现来模拟DbSet
,还尝试通过创建List
实例的Post
并使用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问题很容易解决,但是真正的挑战是找到它们,因为它们不会在测试环境中暴露出来。
期望我可以模拟Entity Framework的
IQueryable
实现的行为是否不切实际?谢谢你的想法
蒂姆
最佳答案
我认为模拟 Entity Framework 的行为非常困难,甚至不可能。首先也是最重要的是,因为这需要对linq-entent与linq-to-objects不同的所有特性和边缘情况的深入了解。正如您所说:真正的挑战是找到它们。让我指出三个主要 Realm ,而不是声称自己几乎是详尽无遗的:
Linq到对象成功而Linq到实体失败的情况:
.Select(x => x.Property1.ToString()
。 LINQ to Entities无法识别方法'System.String ToString()'方法...这几乎适用于本机.Net类中的几乎所有方法,当然也适用于自己的方法。只有少数.Net方法将转换为SQL。参见CLR Method to Canonical Function Mapping。从EF 6.1开始,该方式支持ToString
。但是只有无参数过载。 Skip()
的OrderBy
。 Except
和Intersect
:可能会产生令人讨厌的查询,从而引发您的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
扩展方法。参见Supported and Unsupported LINQ Methods。 .Select(x => new A { Property1 = (x.BoolProperty ? new B { BProp1 = x.Prop1, BProp2 = x.Prop2 } : new B { BProp1 = x.Prop1 }) })
:类型B在here的单个LINQ to Entities查询中出现在两个结构不兼容的初始化中。 context.Entities.Cast<IEntity>()
:无法将类型“Entity”强制转换为类型“IEntity”。 LINQ to Entities仅支持强制转换EDM基本类型或枚举类型。 .Select(p => p.Category?.Name)
。在表达式中使用空传播会引发CS8072表达式树lambda可能不包含空传播运算符。这may get fixed one day。 Linq-to-Object失败并且Linq-to-Entities成功的情况:
.Select(p => p.Category.Name)
:当p.Category
为null时,L2E返回null,但是L2O抛出未设置为对象实例的Object reference。这不能通过使用空传播来解决(请参见上文)。 Nodes.Max(n => n.ParentId.Value)
,其中n.ParentId
具有一些空值。 L2E返回一个最大值,L2O引发Nullable对象必须具有一个值。 EntityFunctions
(从EF 6开始为DbFunctions
)或SqlFunctions
。 都成功/失败但行为不同的情况:
Nodes.Include("ParentNodes")
:L2O没有包含的实现。它将运行并返回节点(如果Nodes
是IQueryable
),但没有父节点。 Nodes.Select(n => n.ParentNodes.Max(p => p.Id))
集合的ParentNodes
:都失败,但有不同的异常(exception)。 Nodes.Where(n => n.Name.Contains("par"))
:L2O区分大小写,L2E取决于数据库排序规则(通常不区分大小写)。 node.ParentNode = parentNode
:具有双向关系,在L2E中,还将节点添加到父节点的节点集合中(关系修正)。不在L2O中。 (请参阅Unit testing a two way EF relationship)。 .Select(p => p.Category == null ? string.Empty : p.Category.Name)
:结果相同,但是生成的SQL查询还包含空检查,可能难以优化。 Nodes.AsNoTracking().Select(n => n.ParentNode
。这个非常棘手! 。使用AsNoTracking
,EF会为每个ParentNode
创建新的Node
对象,因此可以有重复项。没有AsNoTracking
,EF会重复使用现有的ParentNodes
,因为现在涉及到实体状态管理器和实体密钥。可以在L2O中调用AsNoTracking()
,但它不会执行任何操作,因此,无论有没有它,都不会有任何区别。 那么关于模拟延迟/急切加载以及上下文生命周期对延迟加载异常的影响又如何呢?或某些查询构造对性能的影响(例如触发N + 1个SQL查询的构造)。还是由于重复或缺少实体密钥而导致异常?还是关系修正?
我的意见:没有人会伪造那个。最令人担忧的区域是L2O成功和L2E失败的地方。现在,绿色单元测试的值(value)是什么?之前已经说过,EF只能在集成测试(例如here)中可靠地测试,我倾向于同意。
但是,这并不意味着我们应该忘记以EF作为数据层的项目中的单元测试。有ways to do it,但我认为并非没有集成测试。
关于entity-framework - 如何模拟EntityFramework的IQueryable实现的局限性,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/13332002/