NSubstitute icon indicating copy to clipboard operation
NSubstitute copied to clipboard

Proposal: Call-time argument matching for verifying mutable objects in ordered calls

Open loop8ack opened this issue 10 months ago • 1 comments

Is your feature request related to a problem?

I'm encountering the same issue as described in #392 where Received.InOrder evaluates argument matchers at assertion time rather than at the time of the actual call. This leads to false negatives when testing with mutable objects that change during the test.

Consider this example:

var action = Substitute.For<IAction>();
var person = new Person() { Name = "John" };

action.Act(person);
person.Name = "Doe";
action.Act(person);
person.Name = "Hans";

// This fails because both calls are evaluated with person.Name == "Hans"
Received.InOrder(() =>
{
    action.Act(Arg.Is<Person>(p => p.Name == "John"));
    action.Act(Arg.Is<Person>(p => p.Name == "Doe"));
});

In my case, the argument matchers are far more complex than in this simplified example, making workarounds like capturing the state manually impractical.

Describe the solution you'd like

I've started developing a solution like this:

var action = Substitute.For<IAction>();
var person = new Person() { Name = "John" };

// Configure expectations before any calls happen
var verifier = WillReceive.InOrder(action,
    desc =>
    {
        desc.Call(x => x.Act(Arg.Is<Person>(p => p.Name == "John")));
        desc.Call(x => x.Act(Arg.Is<Person>(p => p.Name == "Doe")));
    });

// Each call will be verified in the order it was received.
// If there were any calls skipped, an exception will be thrown.
action.Act(person);
person.Name = "Doe";
action.Act(person);
person.Name = "Hans";

// Are any calls missing at the end of the test?
verifier.Verify();

The core concept is to register the expected calls beforehand and use NSubstitute's .When().Do() to immediately evaluate each call as it happens, storing just a simple boolean result rather than capturing or cloning the arguments. The approach provides reliable verification of mutable objects while completely avoiding the serialization concerns raised in #392 .

I've started building this as a separate utility that works around NSubstitute, but I believe it would be much more valuable as a native feature. The concept is flexible enough to support various verification scenarios while maintaining the simple and intuitive API style of NSubstitute.

I'd be happy to contribute this as a pull request once the design is agreed upon and would appreciate feedback on whether this approach aligns with NSubstitute's philosophy and any suggestions for better integration with the existing codebase.

loop8ack avatar Jan 28 '25 01:01 loop8ack

I really like that this is self-contained without changing other NSubstitute fundamentals. Rather than a specific substitute target, could potentially try to get it working across multiple subs like Received.InOrder:

var verifier = WillReceive.InOrder({
        action.Call(x => x.Act(Arg.Is<Person>(p => p.Name == "John")));
        action.Call(x => x.Act(Arg.Is<Person>(p => p.Name == "Doe")));
    });

That might help strict mock fans a bit too?

@Romfos @zvirja @alexandrnikitin what do you think?

It might also make a good community extension. NSubstitute.Community.* is open for extensions.

dtchepak avatar Mar 23 '25 05:03 dtchepak