如何为含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
相关产品推荐
相关产品推荐

