Search code examples
c#unit-testinggrpcnsubstitute

NSubstitute Received() throws NullReferenceException on Grpc mocked call


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 executed.

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));
}

Solution

  • I raised the same question in the git repository. Short answer: await returns an instance of a null Task. https://github.com/nsubstitute/NSubstitute/issues/735 Recommended workaround is remove await, I'll start using the discard variable instead of a try-catch in these scenarios from this

    try
    {
        await grpcClient.Received(1).GrpcDeleteObjectAsync(Arg.Any<Request>(), null, null, CancellationToken.None);
    }
    catch (NullReferenceException) { }
    

    to this

    _ = grpcClient.Received(1).GrpcDeleteObjectAsync(Arg.Any<Request>(), null, null, CancellationToken.None);