I've been trying for a few days to test a deletion method, but without success.
After debugging the code I realized that the problem is in the FindAsync method returns null and this causes the test to fall into the NotFound()
condition.
As I'm new to the world of C#, .NET, EntityFramework, and Moq, could anyone help me?
- Controller
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();
}
- Test
[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());
}
CodePudding user response:
You are setting up a mock and verifying a mock for FindAsync on an int, when your controller is passing it a long. Therefore you need to set up your mock and verification in a long, not an int. And also set up the FindAsync mock on the DbSet, not on the DbContext.
For example:
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());
Complete working sample here. Full code used to verify below.
// 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 {}
Note that this test doesn't appear particularly useful. Generally when unit testing, we make assertions around the result, not assertions around the implementation details. There's no need to make an assert that FindAsync was called once. It just makes the test more brittle. If you're unit testing the action method, you'd want to make sure that you get a NotFoundResult when you pass in a non-existent item, and a NoContentResult when you pass in an existent item, and that the proper item is removed from the DbSet. Using an in-memory DbContext rather than mocking it will probably make that simpler.