Moq FindAsync 方法 returns 空

Moq FindAsync method returns null

这几天我一直在尝试测试一种删除方法,但没有成功。

调试代码后,我意识到问题出在 FindAsync 方法 returns null 中,这导致测试陷入 NotFound() 条件.

由于我是 C#、.NET、EntityFramework 和 Moq 领域的新手,有人可以帮助我吗?

        public async Task<IActionResult> DeleteTodoItem(long id)
        {
            var todoItem = await _context.TodoItems.FindAsync(id);
            if (todoItem == null)
            {
                return NotFound();
            }

            _context.TodoItems.Remove(todoItem);
            await _context.SaveChangesAsync();

            return NoContent();
        }
    [Fact]
    public async Task DeleteTodoItem_ShouldBeCallFindAsyncMethodOnce()
    {
        var todo = new TodoItem { Id = 1, Name = "test", IsComplete = true };
        
        var mockSet = new Mock<DbSet<TodoItem>>();

        var options = new DbContextOptionsBuilder<TodoContext>()
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .Options;

        var mockContext = new Mock<TodoContext>(options);
        mockContext.Setup(c => c.TodoItems).Returns(mockSet.Object);
        mockContext.Setup(c => c.TodoItems.FindAsync(1)).ReturnsAsync(todo);
        
        var service = new TodoItemsController(mockContext.Object);
        var deleteTodo = await service.DeleteTodoItem(1);
        
        mockSet.Verify(m => m.FindAsync(It.IsAny<TodoItem>()), Times.Once());
        
        
    }

当您的控制器长时间传递它时,您正在为 int 上的 FindAsync 设置模拟并验证模拟。因此,您需要在 long 而不是 int 中设置模拟和验证。并且还在 DbSet 上设置 FindAsync 模拟,而不是在 DbContext 上。

例如:

var todo = new TodoItem { Id = 1, Name = "test", IsComplete = true };
        
var mockSet = new Mock<DbSet<TodoItem>>();
mockSet.Setup(s => s.FindAsync(1L)).ReturnsAsync(todo);

var mockContext = new Mock<TodoContext>();
mockContext.Setup(c => c.TodoItems).Returns(mockSet.Object);
        
var service = new TodoItemsController(mockContext.Object);
var deleteTodo = await service.DeleteTodoItem(1);
        
mockSet.Verify(m => m.FindAsync(1L), Times.Once());

完整的工作示例 here。下面用于验证的完整代码。

// Need package reference to Microsoft.EntityFrameworkCore v6.0.5
// Need package reference to Moq v4.18.1
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Moq;
                    
public class Program
{
    public static async Task Main()
    {
        var todo = new TodoItem { Id = 1, Name = "test", IsComplete = true };
        
        var mockSet = new Mock<DbSet<TodoItem>>();
        mockSet.Setup(s => s.FindAsync(1L)).ReturnsAsync(todo);

        var mockContext = new Mock<TodoContext>();
        mockContext.Setup(c => c.TodoItems).Returns(mockSet.Object);
        
        var service = new TodoItemsController(mockContext.Object);
        var deleteTodo = await service.DeleteTodoItem(1);
        
        mockSet.Verify(m => m.FindAsync(1L), Times.Once());
        Console.WriteLine("Test complete without error");
    }
}

public class TodoContext : DbContext
{
    public virtual DbSet<TodoItem> TodoItems { get; set; }
}

public class TodoItem
{
    public int Id { get; set; }
    
    public string Name { get; set; }
    
    public bool IsComplete { get; set; }
}

public class TodoItemsController
{
    readonly TodoContext _context;
    
    public TodoItemsController(TodoContext context)
    {
        _context = context;
    }
    
    public async Task<IActionResult> DeleteTodoItem(long id)
    {
        var todoItem = await _context.TodoItems.FindAsync(id);

        if (todoItem == null)
        {
            return NotFound();
        }

        _context.TodoItems.Remove(todoItem);
        await _context.SaveChangesAsync();

        return NoContent();
    }
    
    public NotFoundResult NotFound() { return new NotFoundResult(); }
    
    public NoContentResult NoContent() { return new NoContentResult(); }
}

public interface IActionResult{}

public class NotFoundResult : IActionResult {}

public class NoContentResult : IActionResult {}

请注意,此测试似乎不是特别有用。通常在单元测试时,我们会围绕结果进行断言,而不是围绕实现细节进行断言。无需断言 FindAsync 已被调用一次。它只会让测试更脆弱。如果您正在对操作方法进行单元测试,您需要确保在传入 non-existent 项目时获得 NotFoundResult,在传入现有项目时获得 NoContentResult,并且正确的项目从 DbSet 中删除。使用 in-memory DbContext 而不是模拟它可能会使事情变得更简单。