Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue with .ToListAsync() #852

Open
ShadyManu opened this issue Dec 8, 2024 · 2 comments
Open

Issue with .ToListAsync() #852

ShadyManu opened this issue Dec 8, 2024 · 2 comments

Comments

@ShadyManu
Copy link

ShadyManu commented Dec 8, 2024

Hello everyone, I'm having issues with NSubstitute and EF method ToListAsync().

I have already opened the question in stackoverflow:
https://stackoverflow.com/questions/79263044/problem-with-tolistasync-and-nsubstitute-for-unittest

And I'm having the exact same issue for another test, that inside call the context ad the .ToListAsync() method. I'll show the code of this other one so that there are two examples in place. I'm using .NET 8, NSubstitute 5.3.0 and CQRS pattern with MediatR (Command and Queries).
Here is the sample query I have in my code:

public sealed record GetAllTodosQuery : IQuery<List<TodoDto>>;

internal sealed class GetAllTodosQueryHandler(IApplicationDbContext context)
    : IQueryHandler<GetAllTodosQuery, List<TodoDto>>
{
    public async Task<Result<List<TodoDto>>> Handle(GetAllTodosQuery query, CancellationToken cancellationToken)
    {
        var entities = await context.Todos.ToListAsync(cancellationToken);
        return Result<List<TodoDto>>.Success(entities.Adapt<List<TodoDto>>());
    }
}

This is the attempt of writing a test for that use case:

public class GetAllTodosQueryTest
{
    private readonly IApplicationDbContext _context;
    private readonly GetAllTodosQueryHandler _handler;

    public GetAllTodosQueryTest()
    {
        _context = Substitute.For<IApplicationDbContext>();
        _handler = new GetAllTodosQueryHandler(_context);
    }

    [Fact]
    public async Task Handler_ShouldReturnEmptyList_WhenNoEntitiesInsideDatabase()
    {
        // Arrange
        List<TodoEntity> entities = [];
        var cancellationToken = new CancellationTokenSource().Token;
        
        _context.Todos.ToListAsync(Arg.Is(cancellationToken))
            .Returns(entities);
        
        var query = new GetAllTodosQuery();

        // Act
        var result = await _handler.Handle(query, CancellationToken.None);

        // Assert
        Assert.NotNull(result.Data);
        Assert.Empty(result.Data);
    }
}

I'm getting the same error that I get for the other test I showed inside stackoverflow:

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.GetAllTodosQueryTest.Handler_ShouldReturnEmptyList_WhenNoEntitiesInsideDatabase() in /Users/manuelraso/Documents/repo/devops/templates/dotnet-8-minimal-api-cqrs-postgresql/Application.UnitTests/Todos/GetAllTodosQueryTest.cs:line 28
   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 really don't understand what's going on, because I'm actually able to mock the FindAsync() method, but not the ToListAsync(). In another test I have this:

[Fact]
    public async Task Handler_ShouldReturnSuccess_WhenTodoExists()
    {
        // Arrange
        var guid = Guid.NewGuid();
        var cancellationToken = new CancellationTokenSource().Token;
        var entityFound = new TodoEntity
        {
            Id = guid,
            Title = "Test Title",
            Description = "Test description"
        };

        _context.Todos.FindAsync(Arg.Is(guid), Arg.Is(cancellationToken))
            .Returns(entityFound);

        var query = new GetTodoQuery(guid);

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

        // Assert
        Assert.NotNull(result.Data);
        Assert.Equal(entityFound.Id, result.Data.Id);
        Assert.Equal(entityFound.Title, result.Data.Title);
        Assert.Equal(entityFound.Description, result.Data.Description);
    }

and it's working like a charm. Am I doing something wrong? Is there a bug? How can I make this work?

@Ergamon
Copy link

Ergamon commented Dec 9, 2024

I think you have to learn how NSubstitute works. You can only mock methods that are part of the class/interface you are mocking.

ToListAsync is an extension method, which is a static method of a totally different class. You cannot mock these with any mocking library.

To get out, you have to make your abstraction of the DbContext really abstract or you have to handcraft your mock, e.g. by using an in memory database.

The goal of extensions methods is to make code more readable, but sometimes you forget that these are just static methods and static is normally the enemy of testability.

@dtchepak
Copy link
Member

dtchepak commented Dec 9, 2024

@ShadyManu thanks for the comprehensive example. 👍
This is definitely confusing if you haven't encountered this before. In addition to @Ergamon's advice, I recommend adding NSubstitute Analyzers to your test project to help detect these cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants