NSubstitute icon indicating copy to clipboard operation
NSubstitute copied to clipboard

Properties from a substitute object are empty when accessing in a Received.InOrder bloc

Open gabrielheming opened this issue 2 years ago • 0 comments

Describe the bug Properties from a substitute object are empty when accessing in a Received.InOrder bloc. It will be clear checking the tests below.

To Reproduce

This is the simplest end-to-end example that I could get the same behaviour.

public interface IMyClass
{
    public Uri Uri { get; set; }
}

public interface IMyService
{
    public IMyClass GetMyClassAsync();
    public void PassUri(Uri uri);
}

public class SystemUnderTest
{
    private readonly IMyService service;

    public SystemUnderTest(IMyService service)
    {
        this.service = service;
    }

    public void Execute()
    {
        var myClass = service.GetMyClassAsync();
        service.PassUri(myClass.Uri);
    }
}

The following

public class ReceivedInOrderTests
{
    private readonly IMyClass myClass = Substitute.For<IMyClass>();
    private readonly IMyService myService = Substitute.For<IMyService>();
    private readonly SystemUnderTest sut;

    public ReceivedInOrderTests()
    {
        myClass.Uri.Returns(new Uri("https://test.com"));

        myService.Configure().GetMyClassAsync().ReturnsForAnyArgs(myClass);
        sut = new SystemUnderTest(myService);
    }

    // This works fine
    [Fact]
    public void PassMyClass_WithoutReceivedInOrder_ShouldValidateAllCalls()
    {
        // Arrange
        // Act
        sut.Execute();

        // Assert
        myService.Received(1).GetMyClassAsync();
        myService.Received(1).PassUri(Arg.Is<Uri>(uri => uri == myClass.Uri));
    }

    // This doesn't work
    [Fact]
    public void PassMyClass_WithReceivedInOrder_ShouldValidateOrderOfCalls()
    {
        // Arrange
        // Act
        sut.Execute();

        // Assert
        Received.InOrder(() =>
        {
            myService.GetMyClassAsync();
     
            // myClass.Uri is null right here
            myService.PassUri(myClass.Uri);
        });

        // this works and assets that myClass.Uri is not null outside the Received.InOrder bloc
        myClass.Uri.ShouldNotBeNull();
    }
}

What happens is that inside the property myClass.Uri inside Received.InOrder is null. For some reason the properties from the substitute object are being cleared when testing inside the Receive.InOrder.

Actual behaviour

NSubstitute.Exceptions.CallSequenceNotFoundException:

NSubstitute.Exceptions.CallSequenceNotFoundException

Expected to receive these calls in order:

GetMyClassAsync()
PassUri(<null>)

Actually received matching calls in this order:

GetMyClassAsync()

*** Note: calls to property getters are not considered part of the query. *** at NSubstitute.Core.SequenceChecking.SequenceInOrderAssertion.Assert(IQueryResults queryResult) at NSubstitute.Received.InOrder(Action calls) at Vincotte.ViA.Storage.UnitTests.ReceivedInOrderTests.PassMyClass_WithReceivedInOrder_ShouldValidateOrderOfCalls()

It doesn't happens when I implement a concrete class, below will you find the example:

public class MyClass : IMyClass
{
    public Uri Uri { get; set; }
}

The new test:

[Fact]
public void PassMyClass_WithConcreteMyClass_ShouldValidateOrderOfCalls()
{
    // Arrange
    var myConcreteClass = new MyClass
    {
        Uri = new Uri("https://test.com")
    };

    myService.Configure().GetMyClassAsync().ReturnsForAnyArgs(myConcreteClass);

    // Act
    sut.Execute();

    // Assert
    Received.InOrder(() =>
    {
        myService.GetMyClassAsync();

       // Works just fine
        myService.PassUri(myConcreteClass.Uri);
    });

    myClass.Uri.ShouldNotBeNull();
}

Expected behaviour Success

Environment:

  • NSubstitute version: 5.1.0
  • NSubstitute.Analyzers version: CSharp 1.0.16
  • Platform: dotnet6.0 project on Mac

Feel free to ask more questions if the explanation is not enough.

gabrielheming avatar Oct 18 '23 13:10 gabrielheming