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

如何为含LINQ的.NET 8 API方法编写概念正确的单元测试?

单元测试问题求助:EF Core DbContext Mock失败及测试逻辑优化

我正在学习编写单元测试,目前有一个基于C# .NET 8.0的API方法需要测试,技术栈使用XUnit、Entity Framework Core 8、SQL Server 2019。现在我在重构代码分离数据层,目标是无需Mock DbContext,但当前单元测试无法运行,先解决测试本身的概念问题。

API方法代码

[HttpGet("GetABunch")]
public async Task<ActionResult<TodoItem>> GetABunch()
{
    string[] myAnimals = { "dog", "cat" };

    var animals = await _context.TodoItems
        .Where(i => myAnimals.Contains(i.Name))
        .ToListAsync();

    return Ok(animals);
}

报错信息

System.ArgumentException : Can not instantiate proxy of class: TodoApiDb.Models.TodoContext.

Could not find a parameterless constructor. (Parameter 'constructorArguments')

当前单元测试代码

[Fact]
public async Task GetABunch_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);
    mockContext.Setup(c => c.Set<TodoItem>()).Returns(mockSet.Object);

    var controller = new TodoItemsController(mockContext.Object);

    // Act
    var result = await controller.GetABunch();

    // 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. 报错直接原因

报错是因为Moq需要创建TodoContext的代理对象,但你的TodoContext没有无参构造函数,导致无法实例化。不过这只是表面问题,更核心的是你当前的测试逻辑违背了“依赖抽象而非具体实现”的原则,这也是你重构数据层的目标所在。

2. 核心优化:通过抽象分离数据层

重构方向是把数据访问逻辑从控制器中剥离,定义抽象接口,让控制器依赖接口而非具体的DbContext,这样单元测试只需Mock接口,无需处理复杂的EF Core对象。

步骤1:定义数据访问抽象接口

// 数据访问接口
public interface ITodoRepository
{
    Task<List<TodoItem>> GetFilteredTodoItemsAsync(string[] filterNames);
}

// 接口实现类(封装EF Core数据访问逻辑)
public class TodoRepository : ITodoRepository
{
    private readonly TodoContext _context;

    public TodoRepository(TodoContext context)
    {
        _context = context;
    }

    public async Task<List<TodoItem>> GetFilteredTodoItemsAsync(string[] filterNames)
    {
        return await _context.TodoItems
            .Where(i => filterNames.Contains(i.Name))
            .ToListAsync();
    }
}

步骤2:重构控制器依赖抽象

[ApiController]
[Route("[controller]")]
public class TodoItemsController : ControllerBase
{
    // 依赖抽象接口而非具体DbContext
    private readonly ITodoRepository _repository;

    public TodoItemsController(ITodoRepository repository)
    {
        _repository = repository;
    }

    [HttpGet("GetABunch")]
    public async Task<ActionResult<List<TodoItem>>> GetABunch()
    {
        string[] myAnimals = { "dog", "cat" };
        var animals = await _repository.GetFilteredTodoItemsAsync(myAnimals);
        return Ok(animals);
    }
}

步骤3:编写简洁的单元测试

现在只需Mock抽象接口,测试逻辑更清晰,也避免了EF Core相关的Mock复杂度:

[Fact]
public async Task GetABunch_ReturnsFilteredTodoItems()
{
    // Arrange
    var mockRepository = new Mock<ITodoRepository>();
    var expectedItems = new List<TodoItem>
    {
        new TodoItem { Id = 1, Name = "dog" },
        new TodoItem { Id = 2, Name = "cat" }
    };
    // 验证传入的过滤参数是否正确,并返回预设数据
    mockRepository.Setup(r => r.GetFilteredTodoItemsAsync(It.Is<string[]>(arr => arr.SequenceEqual(new[] { "dog", "cat" }))))
                  .ReturnsAsync(expectedItems);

    var controller = new TodoItemsController(mockRepository.Object);

    // Act
    var result = await controller.GetABunch();

    // 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. 临时解决方案(不重构抽象层)

如果暂时不想重构到抽象接口,要解决当前的Mock报错,可以:

  • 给TodoContext添加测试用的无参构造函数:
public class TodoContext : DbContext
{
    // 生产环境构造函数
    public TodoContext(DbContextOptions<TodoContext> options) : base(options) { }

    // 测试用无参构造函数
    protected TodoContext() { }

    public DbSet<TodoItem> TodoItems { get; set; }
}

或者在Mock时传入DbContextOptions参数:

var options = new DbContextOptionsBuilder<TodoContext>().Options;
var mockContext = new Mock<TodoContext>(options);

内容的提问来源于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 14:14:53