Manuel Raso
Manuel Raso

Reputation: 3

Problem with ToListAsync() and NSubstitute for UnitTest

I'm trying to do a test for a unit that is supposed to get all the entities from the database and delete them all. I'm using .NET 8 and CQRS pattern. This is the Command Handler to remove all the entities:

internal sealed class DeleteAllTodosCommandHandler(IApplicationDbContext context) : ICommandHandler<DeleteAllTodosCommand, bool>
{
    public async Task<Result<bool>> Handle(DeleteAllTodosCommand request, CancellationToken cancellationToken)
    {
        var entitiesToRemove = await context.Todos.ToListAsync(cancellationToken);
        context.Todos.RemoveRange(entitiesToRemove);
        await context.SaveChangesAsync(cancellationToken);
        
        return Result<bool>.Success(true);
    }
}

It should be as simple as that: take all the entities from db (there's a global query filter for user id), delete them all and save changes.

In the Unit Test, I'm trying to mock first the behavior of the:

await context.Todos.ToListAsync(cancellationToken);

I'm trying to mock it in that way, but it's not working:

[Fact]
public async Task Handle_ShouldReturnSuccess_WhenTodoExists()
{
    // Arrange
    var cancellationToken = new CancellationTokenSource().Token;
    List<TodoEntity> entities =
    [
        new() { Title = "Test title", Description = "Test description" },
        new() { Title = "Test title2", Description = "Test description2" }
    ];
    _contextMock.Todos.ToListAsync(Arg.Is(cancellationToken))
        .Returns(entities);
    var command = new DeleteAllTodosCommand();

    // Act
    var result = await _handler.Handle(command, cancellationToken);

    // Assert
    Assert.True(result.Data);
}

But it's always returning this error:

NSubstitute.Exceptions.UnexpectedArgumentMatcherException: Argument matchers (Arg.Is, Arg.Any) should only be used in place of member arguments. Do no...

NSubstitute.Exceptions.UnexpectedArgumentMatcherException
Argument matchers (Arg.Is, Arg.Any) should only be used in place of member arguments. Do not use in a Returns() statement or anywhere else outside of a member call.
Correct use:
  sub.MyMethod(Arg.Any<string>()).Returns("hi")
Incorrect use:
  sub.MyMethod("hi").Returns(Arg.Any<string>())
   at NSubstitute.Core.ThreadLocalContext.LastCallShouldReturn(IReturn value, MatchArgs matchArgs)
   at NSubstitute.SubstituteExtensions.ConfigureReturn[T](MatchArgs matchArgs, T returnThis, T[] returnThese)
   at NSubstitute.SubstituteExtensions.Returns[T](Task`1 value, T returnThis, T[] returnThese)
   at Application.UnitTests.Todos.DeleteAllTodosCommandTest.Handle_ShouldReturnSuccess_WhenTodoExists() in /Users/manuelraso/Documents/repo/devops/templates/dotnet-8-minimal-api-cqrs-postgresql/Application.UnitTests/Todos/DeleteAllTodosCommandTest.cs:line 30
   at Xunit.Sdk.TestInvoker`1.<>c__DisplayClass48_0.<<InvokeTestMethodAsync>b__1>d.MoveNext() in /_/src/xunit.execution/Sdk/Frameworks/Runners/TestInvoker.cs:line 276
--- End of stack trace from previous location ---
   at Xunit.Sdk.ExecutionTimer.AggregateAsync(Func`1 asyncAction) in /_/src/xunit.execution/Sdk/Frameworks/ExecutionTimer.cs:line 48
   at Xunit.Sdk.ExceptionAggregator.RunAsync(Func`1 code) in /_/src/xunit.core/Sdk/ExceptionAggregator.cs:line 90

I tried to write many variants of that code but it's not working. Before doing that test, I was able to successfully do the test for the DeleteTodo (by id) in that way:

[Fact]
public async Task Handle_ShouldReturnSuccess_WhenTodoExists()
{
    // Arrange
    var cancellationToken = new CancellationTokenSource().Token;
    var entity = new TodoEntity { Title = "Test title", Description = "Test description" };
 
    _contextMock.Todos.FindAsync(Arg.Is(entity.Id), Arg.Is(cancellationToken))
        .Returns(entity);
    _contextMock.SaveChangesAsync(Arg.Is(cancellationToken))
        .Returns(Task.FromResult(1));
    var command = new DeleteTodoCommand(entity.Id);

    // Act
    var result = await _handler.Handle(command, cancellationToken);

    // Assert
    Assert.True(result.Data);
}

Because the DeleteTodo was done in that way:

internal sealed class DeleteTodoCommandHandler(IApplicationDbContext context) : ICommandHandler<DeleteTodoCommand, bool>
{
    public async Task<Result<bool>> Handle(DeleteTodoCommand command, CancellationToken cancellationToken)
    {
        var entityToRemove = await context.Todos.FindAsync(command.Id, cancellationToken);
        if (entityToRemove is null)
        {
            return Result<bool>.Failure(new ResultError("Not found"));
        }
        
        context.Todos.Remove(entityToRemove);
        var result = await context.SaveChangesAsync(cancellationToken);
        
        return Result<bool>.Success(result > 0);
    }
}

Any idea what am I doing wrong?

Upvotes: 0

Views: 44

Answers (0)

Related Questions