【问题标题】:How to mock the limitations of EntityFramework's implementation of IQueryable如何模拟 EntityFramework 的 IQueryable 实现的局限性
【发布时间】:2012-10-31 04:26:58
【问题描述】:

我目前正在为 MVC4 应用程序中的存储库实现编写单元测试。为了模拟数据上下文,我首先采用了this post 的一些想法,但现在我发现了一些限制,这让我怀疑是否可以正确模拟IQueryable

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

例如,以下 sn-p 用于选择属于预定义类别列表的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();

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

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 实现的行为?

感谢您的想法,

提姆。

【问题讨论】:

  • 这不是单元测试,但如果您执行 ToString()(在 DbQuery 上)或 ToTraceString()(在 ObjectQuery 上)会怎样?它将转储与您的查询相对应的 SqlQuery,这意味着它将通过整个 EF 查询管道,但不会将查询发送到数据库。它应该揭示这样的案例。
  • @Pawel。谢谢 - 这是朝着正确方向迈出的一大步,尽管如果我能以某种方式自动化它会很好。

标签: entity-framework mocking iqueryable


【解决方案1】:

我使用Moq 编写了一些带有Entity Framework 6.1.3 的单元测试,并用它来覆盖IQueryable。请注意,所有应测试的DbSet 都需要标记为virtual。来自微软自己的例子:

查询:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;

namespace TestingDemo
{
    [TestClass]
    public class QueryTests
    {
        [TestMethod]
        public void GetAllBlogs_orders_by_name()
        {
            var data = new List<Blog>
            {
                new Blog { Name = "BBB" },
                new Blog { Name = "ZZZ" },
                new Blog { Name = "AAA" },
            }.AsQueryable();

            var mockSet = new Mock<DbSet<Blog>>();
            mockSet.As<IQueryable<Blog>>().Setup(m => m.Provider).Returns(data.Provider);
            mockSet.As<IQueryable<Blog>>().Setup(m => m.Expression).Returns(data.Expression);
            mockSet.As<IQueryable<Blog>>().Setup(m => m.ElementType).Returns(data.ElementType);
            mockSet.As<IQueryable<Blog>>().Setup(m => m.GetEnumerator()).Returns(0 => data.GetEnumerator());

            var mockContext = new Mock<BloggingContext>();
            mockContext.Setup(c => c.Blogs).Returns(mockSet.Object);

            var service = new BlogService(mockContext.Object);
            var blogs = service.GetAllBlogs();

            Assert.AreEqual(3, blogs.Count);
            Assert.AreEqual("AAA", blogs[0].Name);
            Assert.AreEqual("BBB", blogs[1].Name);
            Assert.AreEqual("ZZZ", blogs[2].Name);
        }
    }
}

插入:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System.Data.Entity;

namespace TestingDemo
{
    [TestClass]
    public class NonQueryTests
    {
        [TestMethod]
        public void CreateBlog_saves_a_blog_via_context()
        {
            var mockSet = new Mock<DbSet<Blog>>();

            var mockContext = new Mock<BloggingContext>();
            mockContext.Setup(m => m.Blogs).Returns(mockSet.Object);

            var service = new BlogService(mockContext.Object);
            service.AddBlog("ADO.NET Blog", "http://blogs.msdn.com/adonet");

            mockSet.Verify(m => m.Add(It.IsAny<Blog>()), Times.Once());
            mockContext.Verify(m => m.SaveChanges(), Times.Once());
        }
    }
}

示例服务:

using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;

namespace TestingDemo
{
    public class BlogService
    {
        private BloggingContext _context;

        public BlogService(BloggingContext context)
        {
            _context = context;
        }

        public Blog AddBlog(string name, string url)
        {
            var blog = _context.Blogs.Add(new Blog { Name = name, Url = url });
            _context.SaveChanges();

            return blog;
        }

        public List<Blog> GetAllBlogs()
        {
            var query = from b in _context.Blogs
                        orderby b.Name
                        select b;

            return query.ToList();
        }

        public async Task<List<Blog>> GetAllBlogsAsync()
        {
            var query = from b in _context.Blogs
                        orderby b.Name
                        select b;

            return await query.ToListAsync();
        }
    }
}

来源:https://docs.microsoft.com/en-us/ef/ef6/fundamentals/testing/mocking

【讨论】:

    【解决方案2】:

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

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

    • .Select(x =&gt; x.Property1.ToString()LINQ to Entities 无法识别“System.String ToString()”方法... 这适用于本机 .Net 类中的几乎所有方法,当然也适用于自己的方法。只有少数 .Net 方法会被翻译成 SQL。见CLR Method to Canonical Function Mapping。从 EF 6.1 开始,顺便支持ToString。但只有无参数重载。
    • Skip() 前面没有OrderBy
    • ExceptIntersect:会产生可怕的查询,这些查询会抛出你的 SQL 语句的某些部分嵌套得太深。重写查询或将其分解为更小的查询。
    • Select(x =&gt; x.Date1 - x.Date2)DbArithmeticExpression 参数必须是数字通用类型。
    • (您的情况).Where(p =&gt; p.Category == category)在此上下文中仅支持原始类型或枚举类型。
    • Nodes.Where(n =&gt; n.ParentNodes.First().Id == 1)方法“First”只能作为最终查询操作。
    • context.Nodes.Last()LINQ to Entities 无法识别方法“...Last...”。这适用于许多其他 IQueryable 扩展方法。见Supported and Unsupported LINQ Methods
    • (请参阅下面的 Slauma 评论):.Select(x =&gt; 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&lt;IEntity&gt;()无法将类型“Entity”转换为类型“IEntity”。 LINQ to Entities 仅支持转换 EDM 基元或枚举类型。
    • .Select(p =&gt; p.Category?.Name)。在表达式中使用 null 传播会引发 CS8072 表达式树 lambda 可能不包含 null 传播运算符。may get fixed one day
    • 这个问题:Why does this combination of Select, Where and GroupBy cause an exception? 让我意识到,EF 甚至不支持整个查询构造,而 L2O 不会有任何问题。

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

    • .Select(p =&gt; p.Category.Name):当p.Category 为null 时,L2E 返回null,但L2O 抛出对象引用未设置为对象的实例。这不能通过使用null 传播来解决(见上文)。
    • Nodes.Max(n =&gt; n.ParentId.Value) 带有一些空值 n.ParentId。 L2E 返回一个最大值,L2O 抛出 Nullable 对象必须有一个值。
    • 使用EntityFunctionsDbFunctions EF 6)或SqlFunctions

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

    • Nodes.Include("ParentNodes"):L2O 没有包含的实现。它将运行并返回节点(如果 NodesIQueryable),但没有父节点。
    • Nodes.Select(n =&gt; n.ParentNodes.Max(p =&gt; p.Id)) 带有一些空的 ParentNodes 集合:两者都失败,但有不同的例外。
    • Nodes.Where(n =&gt; n.Name.Contains("par")):L2O 区分大小写,L2E 取决于数据库排序规则(通常不区分大小写)。
    • node.ParentNode = parentNode:具有双向关系,在 L2E 中,这也会将节点添加到父节点的节点集合中(relationship fixup)。不在 L2O 中。 (见Unit testing a two way EF relationship)。
    • null 传播失败的解决方法:.Select(p =&gt; p.Category == null ? string.Empty : p.Category.Name):结果相同,但生成的 SQL 查询还包含 null 检查,可能更难优化。
    • Nodes.AsNoTracking().Select(n =&gt; n.ParentNode。这个非常棘手! AsNoTracking EF 为每个Node 创建新的ParentNode 对象,因此可以有重复。 没有 AsNoTracking EF 重用现有的ParentNodes,因为现在涉及到实体状态管理器和实体键。 AsNoTracking() 可以在 L2O 中调用,但它不做任何事情,所以有没有它永远不会有区别。

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

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

    但是,这并不意味着我们应该忘记以 EF 作为数据层的项目中的单元测试。有ways to do it,但我认为,并非没有集成测试。

    【讨论】:

    • 更糟糕的是:EF 中的工作取决于所使用的数据库。我现在没有具体的例子,但我有查询在 SQL Server 2000 上失败,但在 2005+ 上工作。可能还有一些查询可以在 SQL Server 上运行,但在(例如)MySQL 上失败。
    • 哇!这是一个非常详细的答案,非常有启发性。这不是我想听到的,但它迫使我进行现实检查(并且可能使我免于浪费大量时间)。谢谢。
    • 太棒了!这是一个很棒的详细集合!我必须喜欢这个作为 LINQ != LINQ! 的终极示例!对于第 1 部分,我还有一个:.Select(x =&gt; new A { Property1 = (x.BoolProperty ? new B { BProp1 = x.Prop1, BProp2 = x.Prop2 } : new B { BProp1 = x.Prop1 }) })类型“B”出现在单个 LINQ to Entities 查询等中的两个结构不兼容的初始化中,等等。(来自此处:stackoverflow.com/questions/10904375/…
    • @Slauma。谢谢!到目前为止从未遇到过这个,但我可以想象它也会咬我的场景。我将它添加到第一类。
    • @user89861 我知道这个工具,但我对使用它很谨慎,因为它在测试环境中添加了一个复杂的第三方工具,我不知道它是否总是表现正确。它基本上是基于文件的数据库的查询提供程序。甚至不同的 RDMBS 查询提供程序的行为也不同!此外,如果您无法通过自己的应用程序前端进行维护,请不要低估有意义的模拟数据和测试用例的维护。
    猜你喜欢
    • 2011-10-26
    • 1970-01-01
    • 2019-02-09
    • 2011-01-15
    • 2020-01-09
    • 1970-01-01
    • 2019-10-23
    • 2022-01-22
    • 2018-01-14
    相关资源
    最近更新 更多