如何对使用EF.Constant的LINQ查询进行单元测试?
解决EF.Constant在Mock单元测试中报错的问题
我使用Entity Framework Core 8.13搭配兼容级别120的SQL Server 2014数据库,为了适配SQL Server 2014的查询语法,在LINQ to SQL查询中使用EF.Constant方法,代码在生产环境正常运行,但用XUnit 2.4和Moq 4.18编写单元测试时,触发如下错误:
The EF.Constant method may only be used within Entity Framework LINQ queries.
我清楚正常运行时LINQ会被转换为SQL执行,但Mock环境下无法识别EF Core特有的EF.Constant方法,导致报错。以下是我的示例代码及无效测试代码,以及可行的解决方案:
示例API代码
[HttpGet("GetABunch")] public async Task<ActionResult<TodoItem>> GetABunch() { string[] myAnimals = { "dog", "cat" }; // 适用于SQL Server 2017+的写法 var animals = await _context.TodoItems .Where(i => myAnimals.Contains(i.Name)) .ToListAsync(); return Ok(animals); } [HttpGet("GetABunch120")] public async Task<ActionResult<TodoItem>> GetABunch120() { string[] myAnimals = { "dog", "cat" }; // 适配SQL Server 2014的写法(使用EF.Constant) var animals = await _context.TodoItems .Where(i => EF.Constant(myAnimals).Contains(i.Name)) .ToListAsync(); return Ok(animals); }
无效的单元测试代码
[Fact] public async Task GetABunch120_ReturnsFilteredTodoItems() { // Arrange var mockData = new List<TodoItem> { new TodoItem { Id = 1, Name = "dog" }, new TodoItem { Id = 2, Name = "cat" }, new TodoItem { Id = 3, Name = "bird" } }.AsQueryable(); var mockSet = new Mock<DbSet<TodoItem>>(); mockSet.As<IQueryable<TodoItem>>().Setup(m => m.Provider).Returns(mockData.Provider); mockSet.As<IQueryable<TodoItem>>().Setup(m => m.Expression).Returns(mockData.Expression); mockSet.As<IQueryable<TodoItem>>().Setup(m => m.ElementType).Returns(mockData.ElementType); mockSet.As<IQueryable<TodoItem>>().Setup(m => m.GetEnumerator()).Returns(mockData.GetEnumerator()); // Mock EF Core的异步枚举 mockSet.As<IAsyncEnumerable<TodoItem>>() .Setup(m => m.GetAsyncEnumerator(It.IsAny<CancellationToken>())) .Returns(new TestAsyncEnumerator<TodoItem>(mockData.GetEnumerator())); var mockContext = new Mock<TodoContext>(); mockContext.Setup(c => c.TodoItems).Returns(mockSet.Object); var controller = new TodoItemsController(mockContext.Object); // Act var result = await controller.GetABunch120(); // Assert var actionResult = Assert.IsType<OkObjectResult>(result.Result); var returnedItems = Assert.IsType<List<TodoItem>>(actionResult.Value); Assert.Equal(2, returnedItems.Count); Assert.Contains(returnedItems, i => i.Name == "dog"); Assert.Contains(returnedItems, i => i.Name == "cat"); }
可行解决方案
方法1:改用EF Core InMemory数据库
Mock的DbSet无法处理EF Core专属的函数(如EF.Constant),改用InMemory数据库可以模拟真实的EF Core查询执行环境,步骤如下:
- 安装NuGet包:
Microsoft.EntityFrameworkCore.InMemory - 编写测试代码:
[Fact] public async Task GetABunch120_ReturnsFilteredTodoItems() { // Arrange:创建InMemory数据库上下文配置 var options = new DbContextOptionsBuilder<TodoContext>() .UseInMemoryDatabase(databaseName: "Test_Todo_Db") .Options; // 填充测试数据 using (var context = new TodoContext(options)) { context.TodoItems.AddRange( new TodoItem { Id = 1, Name = "dog" }, new TodoItem { Id = 2, Name = "cat" }, new TodoItem { Id = 3, Name = "bird" } ); await context.SaveChangesAsync(); } // 创建控制器并执行测试 using (var context = new TodoContext(options)) { var controller = new TodoItemsController(context); // Act var result = await controller.GetABunch120(); // Assert var actionResult = Assert.IsType<OkObjectResult>(result.Result); var returnedItems = Assert.IsType<List<TodoItem>>(actionResult.Value); Assert.Equal(2, returnedItems.Count); Assert.Contains(returnedItems, i => i.Name == "dog"); Assert.Contains(returnedItems, i => i.Name == "cat"); } }
方法2:封装EF.Constant调用并Mock封装服务
如果必须使用Mock,可以将EF.Constant的调用封装到可Mock的服务中,隔离EF Core依赖:
- 创建常量提供接口与实现:
public interface IConstantProvider { T Constant<T>(T value); } public class ConstantProvider : IConstantProvider { public T Constant<T>(T value) => EF.Constant(value); }
- 注册服务(Program.cs/Startup.cs):
services.AddScoped<IConstantProvider, ConstantProvider>();
- 修改控制器注入并使用该服务:
private readonly TodoContext _context; private readonly IConstantProvider _constantProvider; public TodoItemsController(TodoContext context, IConstantProvider constantProvider) { _context = context; _constantProvider = constantProvider; } [HttpGet("GetABunch120")] public async Task<ActionResult<TodoItem>> GetABunch120() { string[] myAnimals = { "dog", "cat" }; var animals = await _context.TodoItems .Where(i => _constantProvider.Constant(myAnimals).Contains(i.Name)) .ToListAsync(); return Ok(animals); }
- 编写Mock测试代码:
[Fact] public async Task GetABunch120_ReturnsFilteredTodoItems() { // Arrange var mockData = new List<TodoItem> { new TodoItem { Id = 1, Name = "dog" }, new TodoItem { Id = 2, Name = "cat" }, new TodoItem { Id = 3, Name = "bird" } }.AsQueryable(); var mockSet = new Mock<DbSet<TodoItem>>(); mockSet.As<IQueryable<TodoItem>>().Setup(m => m.Provider).Returns(mockData.Provider); mockSet.As<IQueryable<TodoItem>>().Setup(m => m.Expression).Returns(mockData.Expression); mockSet.As<IQueryable<TodoItem>>().Setup(m => m.ElementType).Returns(mockData.ElementType); mockSet.As<IQueryable<TodoItem>>().Setup(m => m.GetEnumerator()).Returns(mockData.GetEnumerator()); mockSet.As<IAsyncEnumerable<TodoItem>>() .Setup(m => m.GetAsyncEnumerator(It.IsAny<CancellationToken>())) .Returns(new TestAsyncEnumerator<TodoItem>(mockData.GetEnumerator())); var mockContext = new Mock<TodoContext>(); mockContext.Setup(c => c.TodoItems).Returns(mockSet.Object); // Mock常量提供服务,直接返回输入值 var mockConstantProvider = new Mock<IConstantProvider>(); mockConstantProvider.Setup(p => p.Constant(It.IsAny<string[]>())) .Returns<string[]>(input => input); var controller = new TodoItemsController(mockContext.Object, mockConstantProvider.Object); // Act var result = await controller.GetABunch120(); // Assert var actionResult = Assert.IsType<OkObjectResult>(result.Result); var returnedItems = Assert.IsType<List<TodoItem>>(actionResult.Value); Assert.Equal(2, returnedItems.Count); Assert.Contains(returnedItems, i => i.Name == "dog"); Assert.Contains(returnedItems, i => i.Name == "cat"); }
方法3:自定义LINQ表达式访问者(进阶)
如果不想修改业务代码,可以通过自定义LINQ表达式访问者,在Mock环境中将EF.Constant的调用替换为直接返回常量值。这种方法实现复杂,适合需要保持业务代码完全不变的场景,核心思路是遍历LINQ表达式树,替换EF.Constant的调用节点。
内容的提问来源于stack exchange,提问作者sam sergiy klok
相关产品推荐
相关产品推荐

