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

时间:2012-11-11 14:25:49

标签: entity-framework mocking iqueryable

我目前正在为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,并创建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的实施行为,这是不现实的吗?

感谢您的想法,

2 个答案:

答案 0 :(得分:61)

我认为模拟实体框架行为是非常困难的,如果不可能的话。首先是因为它需要对所有特性和边缘情况有深刻的了解,其中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 Method to Canonical Function Mapping。从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扩展方法。见Supported and Unsupported LINQ Methods
  • (请参阅下面的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查询中的两个结构上不兼容的初始化... 来自here
  • context.Entities.Cast<IEntity>()无法将“实体”类型转换为“IEntity”类型。 LINQ to Entities仅支持转换EDM原语或枚举类型。
  • .Select(p => p.Category?.Name)。在表达式中使用空传播抛出 CS8072表达式树lambda可能不包含空传播运算符。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 => p.Category.Name):当p.Category为空时,L2E返回null,但L2O抛出对象引用未设置为对象的实例。这不能通过使用来修复零传播(见上文)。
  • Nodes.Max(n => n.ParentId.Value)n.ParentId有一些空值。 L2E返回最大值,L2O抛出 Nullable对象必须有值。
  • 使用EntityFunctions(EF 6中的DbFunctions)或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中,这也会将节点添加到父节点集合( relationship fixup )。不在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失败。现在绿色单元测试的价值是多少?之前已经说过EF只能在集成测试中可靠地进行测试(例如here),我倾向于同意。

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

答案 1 :(得分:0)

我使用Entity Framework 6.1.3Moq编写了一些单元测试,并用它覆盖了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