You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何对使用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查询执行环境,步骤如下:

  1. 安装NuGet包:Microsoft.EntityFrameworkCore.InMemory
  2. 编写测试代码:
[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依赖:

  1. 创建常量提供接口与实现:
public interface IConstantProvider
{
    T Constant<T>(T value);
}

public class ConstantProvider : IConstantProvider
{
    public T Constant<T>(T value) => EF.Constant(value);
}
  1. 注册服务(Program.cs/Startup.cs):
services.AddScoped<IConstantProvider, ConstantProvider>();
  1. 修改控制器注入并使用该服务:
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);
}
  1. 编写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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 16:41:00