NSubstitute
NSubstitute copied to clipboard
Received check passes if object's property is modified to match predicate after function call
Describe the bug
Received passes even if the predicate isn't true when calling the method being asserted, if the property that the predicate is checking is changed after the method call.
To Reproduce Clazz.cs
namespace TestProject1
{
public class A
{
private readonly IB _b;
public A(IB b)
{
_b = b;
}
public void Foo()
{
// Explicitly set Str to null. Should cause test to fail.
var arg = new TestArg {Str = null};
_b.Bar(arg);
// Now change Str to a value that satisfies the Received predicate
arg.Str = "test";
}
}
public class TestArg
{
public string Str { get; set; }
}
public interface IB
{
void Bar(TestArg testArg);
}
public class B : IB
{
public void Bar(TestArg testArg)
{
;
}
}
}
UnitTest1.cs
using NSubstitute;
using Xunit;
namespace TestProject1
{
public class Tests
{
[Fact]
public void Test()
{
var b = Substitute.For<IB>();
var sut = new A(b);
sut.Foo();
// Str should not be empty, but when we call Bar it actually is. Yet, test passes.
b.Received(1).Bar(Arg.Is<TestArg>(s => !string.IsNullOrEmpty(s.Str)));
}
}
}
Expected behaviour
Since _b.Bar is called with the incorrect arguments, i.e. a null/empty string, the Received() assertion should fail. It should not matter if the property being tested is changed after the call.
Environment:
- NSubstitute version: 4.2.2
- NSubstitute.Analyzers version: 1.0.14
- Platform: netcoreapp3.1 on Mac
Additional context
I have verified that this bug does not occur when used with a single value, i.e. for example if the signature of Bar was void Bar(string str) or void Bar(Guid? str).
The same behaviour happens if the property of TestArg is a class, for example
public class TestArg
{
public C Prop { get; set; }
}
Thanks for the report @Jokab.
This is a bit of a tricky case for NSubstitute. Argument assertions work using references, so this case would also pass:
var arg = new TestArg { Str = "hi" };
_b.Bar(arg);
arg.Str = "blah";
_b.Received().Bar(arg);
So there are a few issues. If we were to deep clone the arg, we could detect that arg has changed... but that would cause this test to fail which would also be confusing. It would also cause us issues with memory usage.
A compromise is to save the value used at the time of call using When..Do or Arg.Do:
String savedArg = "";
_b.Bar(Arg.Do<TestArg>(x => savedArg = x.Str));
// ... code gets called ...
Assert.AreEqual("hi", savedArg);
Hope this helps.
Hi again. Sorry for the delay.
Thanks, your solution worked.
It's a bit unintuitive that I can't do what I want - would it be possible to somehow clarify this in the documentation?