30

我目前正在为 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();

在我的测试环境中,我尝试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 问题很容易修复,但真正的挑战是找到它们,因为它们不会在测试环境中暴露出来。

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

谢谢你的想法,

蒂姆。

4

2 回答 2

67

我认为模拟实体框架行为非常非常困难,如果不可能的话。首先也是最重要的,因为它需要深入了解 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
  • Exceptand 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扩展方法。请参阅支持和不支持的 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>():无法将类型“实体”转换为类型“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.Categoryis null L2E 返回 null,但 L2O 抛出Object reference not set to an object of an instance。这不能通过使用空传播来解决(见上文)。
  • Nodes.Max(n => n.ParentId.Value)有一些空值n.ParentId。L2E 返回一个最大值,L2O 抛出Nullable 对象必须有一个值。
  • 使用EntityFunctionsDbFunctions从 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. 这个非常棘手!. 使用 AsNoTrackingEFParentNode为 each 创建新对象Node,因此可以有重复项。没有 AsNoTrackingEF 重用 existing ParentNodes,因为现在涉及到实体状态管理器和实体键。AsNoTracking()可以在 L2O 中调用,但它不做任何事情,因此有无它永远不会有区别。

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

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

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

于 2012-11-12T22:40:46.570 回答
2

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

于 2018-08-25T20:55:21.863 回答