Search code examples
c#nsubstitute

Why does registering another return value on an NSubstitute mock with a different value for an argument matcher throw a NullReferenceException


I've got a .NET Core Xunit project targeting .NET 4.6 which is using NSubstitute 3.1.0. I'm setting up a mocked object which needs to return different things based on different arguments received. Here's some sample code (and a link to the github repo for the source code of the full project):

public class UnitTest1
{
    private IFoo getMockFoo()
    {
        int instanceIDPool = 1;
        IFoo foo = Substitute.For<IFoo>();
        foo.AddBar(Arg.Is<BarInfoObj>(x => x.ServerName.Contains("SuccessAddNewBar"))).Returns(ci =>
        {
            BarInfoObj barInfoObjToReturn = ci.ArgAt<BarInfoObj>(0).ShallowCopy();
            barInfoObjToReturn.BarInstanceId = instanceIDPool++;
            barInfoObjToReturn.Status = BarStatus.Idle;
            barInfoObjToReturn.StatusMessage = "Newly registered Bar";
            barInfoObjToReturn.StatusReportDateTimeUTC = TimeZoneInfo.ConvertTimeToUtc(DateTime.Now);
            string msg = $"Added Bar at {barInfoObjToReturn.BarUrl} with instance ID {barInfoObjToReturn.BarInstanceId.ToString()} to the Foo's pool of Bars.";
            return Response<BarInfoObj>.Success(barInfoObjToReturn, msg);
        });
        //This second return value registration throws a NullReferenceException.
        //This won't throw an exception if I comment out the return registration above; the
        //exception is thrown whenever there is more than one return value registration.
        foo.AddBar(Arg.Is<BarInfoObj>(x => x.ServerName.Contains("SuccessAddExistingBar"))).Returns(ci =>
        {
            BarInfoObj barInfoObjToReturn = ci.ArgAt<BarInfoObj>(0).ShallowCopy();
            barInfoObjToReturn.BarInstanceId = instanceIDPool++;
            barInfoObjToReturn.Status = BarStatus.Idle;
            barInfoObjToReturn.StatusMessage = "Re-registered Bar";
            barInfoObjToReturn.StatusReportDateTimeUTC = TimeZoneInfo.ConvertTimeToUtc(DateTime.Now);
            string msg = $"Re-registered Bar at {barInfoObjToReturn.BarUrl} with instance ID {barInfoObjToReturn.BarInstanceId.ToString()}.";
            return Response<BarInfoObj>.Success(barInfoObjToReturn, msg);
        });

        return foo;
    }

    [Fact]
    public void Test1()
    {
        //Arrange
        IFoo testFoo = getMockFoo();
        BarInfoObj testInputBar = new BarInfoObj()
        {
            ServerName = "SuccessAddNewBar",
            BarUrl = "http://some.test.url",
            BarInstanceId = 1,
            Status = BarStatus.Idle,
            StatusMessage = "TestInput",
            StatusReportDateTimeUTC = DateTime.Now
        };

        //Act
        Response<BarInfoObj> testResponse = testFoo.AddBar(testInputBar);

        //Assert
        //Just some arbitrary test; the test isn't important because the exception happens
        //when creating the mock.
        testFoo.ReceivedCalls();
    }
}

When I was debugging the unit test code I noticed that the second return value registration for the AddBar method throws a NullReferenceException. I've tried commenting out each one and, when they run on their own, they're fine; no exceptions are thrown. The exception gets thrown when there are two or more return value registrations for the AddBar method. If I just run the unit test, without debugging, no exceptions get thrown and the test will succeed. I've done something similar in another set of unit tests which don't throw any exceptions and I can't figure out why this one has a problem. I've followed the NSubstitute documenation and it appears that this should be doable. I'm hoping that another pair of eyes might help to point me in the right direction.


Solution

  • Thanks for the repo. I believe this is occurring as the process of stubbing the second call ends up invoking the first stub with a null argument (Arg.Is returns null). NSubstitute realises that this call doesn't match, and so handles the NullReferenceException. The test should still work as required -- it just affects debugging when "break on exception" is enabled.

    If you update to NSubstitute 4.x this case should be handled without the exception as there is some code to detect that a call is being stubbed and so the first stub will not be checked.