Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Argument matchers fail if null passed in to an out parameter of a generic derived type #828

Open
cory-hrh opened this issue Sep 2, 2024 · 0 comments
Labels
bug Reported problem with NSubstitute behaviour help wanted Core team needs help with this! If you've got some time, please volunteer to take a look.

Comments

@cory-hrh
Copy link

cory-hrh commented Sep 2, 2024

Describe the bug
Argument matchers for generic out parameters fail for derived methods where the out is a derived type and null is passed in.
(The reproduce code should make it clearer)

To Reproduce

CodeUnderTest.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

SomeClass.cs

namespace CodeUnderTest
{
    public interface IOutBase { }

    public interface IOutDerived : IOutBase { }

    public interface IServiceBase
    {
        void TryGet<T>(out T value) where T : IOutBase;
    }

    public interface IServiceDerived : IServiceBase
    {
        new void TryGet<T>(out T value) where T : IOutDerived;
    }

    public class SomeClass
    {
        readonly IServiceBase service;

        public SomeClass(IServiceBase service)
        {
            this.service = service;
        }

        public void SomeMethod()
        {
            ((IServiceDerived)this.service).TryGet(out IOutDerived _); // Call derived version

            this.service.TryGet(out IOutBase _); // Call base version
        }
    }
}

TestProject.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>

    <IsPackable>false</IsPackable>
    <IsTestProject>true</IsTestProject>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="coverlet.collector" Version="6.0.0" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
    <PackageReference Include="NSubstitute" Version="5.1.0" />
    <PackageReference Include="NSubstitute.Analyzers.CSharp" Version="1.0.17">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="NUnit" Version="4.2.2" />
    <PackageReference Include="NUnit.Analyzers" Version="4.3.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\CodeUnderTest\CodeUnderTest.csproj" />
  </ItemGroup>

  <ItemGroup>
    <Using Include="NUnit.Framework" />
  </ItemGroup>

</Project>

TestSomeClass.cs

using NSubstitute;

namespace CodeUnderTest
{
    public class TestSomeClass
    {
        IServiceDerived service;
        SomeClass instance;

        [SetUp]
        public void Setup()
        {
            this.service = Substitute.For<IServiceDerived>();
            this.instance = new SomeClass(this.service);
        }

        [Test]
        public void TestMethod()
        {
            // Act
            this.instance.SomeMethod();

            // Assert
            this.service.Received(1).TryGet(out Arg.Any<IOutDerived>()); // Fails as 2 matching calls received
            ((IServiceBase)this.service).Received(1).TryGet(out Arg.Any<IOutBase>()); // Fails as 2 matching calls received
        }
    }
}

Expected behaviour
The test TestMethod to pass correctly, asserting that one call was received by each method.
Actually both asserts will fail as they are unable to differentiate between the two calls even though the generics do not match.

NSubstitute.Exceptions.ReceivedCallsException : Expected to receive exactly 1 call matching:
    TryGet<IOutDerived>(any IOutDerived)
Actually received 2 matching calls:
    TryGet<IOutDerived>(<null>)
    TryGet<IOutBase>(<null>)
NSubstitute.Exceptions.ReceivedCallsException : Expected to receive exactly 1 call matching:
    TryGet<IOutBase>(any IOutBase)
Actually received 2 matching calls:
    TryGet<IOutDerived>(<null>)
    TryGet<IOutBase>(<null>)

Environment:
See csproj files.
This was originally seen on .NET 472 and NSubstitute 4.2.1 but when creating the minimal example above it was confirmed to still be present in .NET 8 and NSubstitute 5.1.0

Additional context

  • The original code actually created mocks for both interfaces using .Returns(..) and it was seen that the incorrect mocks were being hit by the code being tested, i.e. both interface calls would go to the same mock
  • It was also seen that using .When().Do() to create the mocks resulted in both mocks being hit in sequence. So both interface calls would hit both mocks
  • The incorrect behaviour was still seen with the mocks removed so they aren't part of the reproducible code
  • If a non-null instance of either IOutBase or IOutDerived is passed in to the out parameter then the correct mock would get hit
@dtchepak dtchepak added bug Reported problem with NSubstitute behaviour help wanted Core team needs help with this! If you've got some time, please volunteer to take a look. labels Oct 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Reported problem with NSubstitute behaviour help wanted Core team needs help with this! If you've got some time, please volunteer to take a look.
Projects
None yet
Development

No branches or pull requests

2 participants