我目前正在为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问题很容易解决,但真正的挑战是找到它们,因为它们没有在测试环境中显示出来。
我期望我可以嘲笑实体框架IQueryable
的实施行为,这是不现实的吗?
感谢您的想法,
添
答案 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
。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”出现在单个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。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没有包含的实现。它将运行并返回节点(如果Nodes
为IQueryable
),但没有父节点。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.3
用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