NSubstitute icon indicating copy to clipboard operation
NSubstitute copied to clipboard

GetArguments is returning the latest call for all calls if we're using the same reference type

Open hartmark opened this issue 1 year ago • 3 comments

Describe the bug It seems nsubstitute records all calls by reference rather than cloning the object.

To Reproduce Run the XUnit test below.

Expected behaviour All tests should pass. Or we should have a code analyzer hinting us that .Received(1) with argument matcher on reference types can have unexpected behaviour.

Environment:

  • NSubstitute version: [5.1.0]
  • NSubstitute.Analyzers version: [CSharp 1.0.17]
  • Platform: [net8 windows10]

Reproducing test

using System;
using System.Linq;
using NSubstitute;
using Xunit;
using Xunit.Abstractions;

namespace UnitTests.Tests;

public class GetArgumentsTest(ITestOutputHelper testOutputHelper)
{
    [Fact]
    public void ArgumentMatcher_OnSameInstance_ValueType()
    {
        var substitute = Substitute.For<IObjectUnderTest>();

        var value = "initialValue";
        substitute.Foo(value);
        value = "newValue";
        substitute.Foo(value);

        var args = substitute.ReceivedCalls().SelectMany(x => x.GetArguments());
        testOutputHelper.WriteLine(string.Join(", ", args));
    }
    
    [Fact]
    public void ArgumentMatcher_OnSameInstance_ReferenceType_SameInstance()
    {
        var substitute = Substitute.For<IObjectUnderTest>();

        var value = new MyReferenceType { Value = "initialValue"};
        substitute.Foo(value);
        value.Value = "newValue";
        substitute.Foo(value);

        var args = substitute.ReceivedCalls().SelectMany(x => x.GetArguments());
        testOutputHelper.WriteLine(string.Join(", ", args));
        
        substitute.Received(1).Foo(Arg.Is<MyReferenceType>(x => x.Value == "initialValue"));
        substitute.Received(1).Foo(Arg.Is<MyReferenceType>(x => x.Value == "newValue"));
    }
    
    [Fact]
    public void ArgumentMatcher_OnSameInstance_ReferenceType_NewInstance()
    {
        var substitute = Substitute.For<IObjectUnderTest>();

        var value = new MyReferenceType { Value = "initialValue"};
        substitute.Foo(value);
        value = new MyReferenceType { Value = "newValue"};
        substitute.Foo(value);

        var args = substitute.ReceivedCalls().SelectMany(x => x.GetArguments());
        testOutputHelper.WriteLine(string.Join(", ", args));
        
        substitute.Received(1).Foo(Arg.Is<MyReferenceType>(x => x.Value == "initialValue"));
        substitute.Received(1).Foo(Arg.Is<MyReferenceType>(x => x.Value == "newValue"));
    }
}

public class MyReferenceType
{
    public override string ToString()
    {
        return $"{nameof(Value)}: {Value}";
    }

    public string Value { get; set; }
}

public interface IObjectUnderTest
{
    public void Foo(object argument);
}

hartmark avatar May 17 '24 09:05 hartmark