NSubstitute icon indicating copy to clipboard operation
NSubstitute copied to clipboard

NSubstitute Received() throws System.NullReferenceException on Grpc mocked call

Open JosimarTT opened this issue 2 years ago • 3 comments

repo example https://github.com/JosimarTT/NSubstitute-GrpcClient-TestFail

I'm migrating from Moq to Nsubstitute and faced this problem. In Moq I have an unit test to verify that the grpc method was called once. Moving to NSubstitute the assertion throws NullReferenceException. I read that NSubstitute is not meant for classes but interfaces or virtual methods. I think in this case it meet the requirements. The grpc methods are from a nuget that I implement in my code.

I tested adding more calls or reduce them and NSubstitute throws the expected error, something like "Expected to receive exactly 2 calls matching, Actually received 1 matching call". So, the problem is just when I want to assert exactly how many calls I expect the method was called.

Here is the test in question, commented lines are from Moq

[TestClass]
public class MyRepositoryTests : TestBaseV2
{
    private GrpcService.GrpcServiceClient grpcClient;
    private IMyRepositoryConverter myRepositoryConverter;
    private ILogger<MyRepository> logger;

    [TestInitialize]
    public void Init()
    {
        grpcClient = Substitute.For<GrpcService.GrpcServiceClient>();
        myRepositoryConverter = Substitute.For<IMyRepositoryConverter>();
        logger = Substitute.For<ILogger<MyRepository>>();
    }

    [TestCleanup]
    public void CleanUp()
    {
        grpcClient = null;
        myRepositoryConverter = null;
        logger = null;
    }

    public MyRepository GetMyRepository() => new(grpcClient, myRepositoryConverter, logger);

    [TestMethod]
    public async Task DeleteObjectAsync_ExecutesWithSuccess_NoError()
    {
        Guid Id = Guid.NewGuid();
        var grpcResponse = AutoFixture.Build<ObjectResponse>()
                                    .With(r => r.Success, true)
                                    .With(r => r.Error, "")
                                    .Create();

        var mockCall = TestCalls.AsyncUnaryCall(Task.FromResult(grpcResponse),
                                                Task.FromResult(new Metadata()),
                                                () => Status.DefaultSuccess,
                                                () => new Metadata(),
                                                () => { });

        // grpcClient.Setup(c => c.GrpcDeleteObjectAsync(It.IsAny<ObjectRequest>(), null, null, CancellationToken.None)).Returns(mockCall);
        grpcClient.GrpcDeleteObjectAsync(Arg.Any<ObjectRequest>(), null, null, CancellationToken.None).Returns(mockCall);

        var repo = GetMyRepository();
        await repo.DeleteObjectAsync(Id);


        // grpcClient.Verify(c => c.GrpcDeleteObjectAsync(It.IsAny<ObjectRequest>(), null, null, CancellationToken.None), Times.Once());
        try
        {
            await grpcClient.Received(1).GrpcDeleteObjectAsync(Arg.Any<Request>(), null, null, CancellationToken.None);
        }
        catch (NullReferenceException) { }
    }
}

This is what I see when navigate to GrpcDeleteObjectAsync method

[GeneratedCode("grpc_csharp_plugin", null)]
public virtual AsyncUnaryCall<ObjectResponse> GrpcDeleteObjectAsync(ObjectRequest request, Metadata headers = null, DateTime? deadline = null, CancellationToken cancellationToken = default(CancellationToken))
{
    return GrpcDeleteObjectAsync(request, new CallOptions(headers, deadline, cancellationToken));
}

JosimarTT avatar Sep 08 '23 15:09 JosimarTT

Hi @JosimarTT ,

This seems similar to https://github.com/nsubstitute/NSubstitute/issues/722 ? Would you be able to post a minimal repro I can run?

dtchepak avatar Sep 09 '23 04:09 dtchepak

@dtchepak I updated my post with an example repo

JosimarTT avatar Sep 10 '23 06:09 JosimarTT

The problem is the await of the result from the Received call.

Basically this is how NSubstitute works. The Received method call is to check, if the right action has happened. It is more or less a side effect that it also returns something.

The result is generated by the NSubstitute rules. If it has a parameterless constructor, it is generated. If it requires parameters, it returns null.

So the test passes and works as expected, but after your assertion you await a Task. But this task instance is null.

Just remove the await and everything works you want.

Sadly a lot of analyzers spot the missing await and tell you that it is needed. But sadly the contrary is the case.

Ergamon avatar Sep 10 '23 18:09 Ergamon

I think this issue has been resolved with the answer of @Ergamon (thanks) and therefore I will close this one.

Please let us know if you need further information or would like us to take another look at this

304NotModified avatar Apr 29 '24 11:04 304NotModified