Argument matchers fail if null passed in to an out parameter of a generic derived type
cory-hrh opened this issue · 0 comments
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
orIOutDerived
is passed in to the out parameter then the correct mock would get hit