NSubstitute icon indicating copy to clipboard operation
NSubstitute copied to clipboard

Add WillReceive.InOrder() for evaluating call sequence expectations at execution time

Open loop8ack opened this issue 10 months ago • 2 comments

This PR introduces WillReceive.InOrder() to solve the issue where mutable objects lead to false negatives in ordered call verifications. The core problem is that Received.InOrder() evaluates argument matchers at verification time instead of call time, causing tests to fail when object references are modified during test execution.

This addresses the long-standing issue #392 and implements the solution I proposed in #861.

The syntax of this feature:

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

WillReceive
    .InOrder(() =>
    {
        action.Act(Arg.Is<Person>(p => p.Name == "John"));
        action.Act(Arg.Is<Person>(p => p.Name == "Joe"));
    })
    .WhileExecuting(() =>
    {
        action.Act(person);
        person.Name = "Doe";
        action.Act(person);
        person.Name = "Hans";
    });

Produces clear error messages showing the exact point of failure:

Call 1: Accepted!
Call 2: Not matched!
    Expected: Act(p => (p.Name == "Joe"))
    But was: Act(Person)
        arg[0] not matched: p => (p.Name == "Joe")

The goal was to provide a solution that evaluates argument matchers at the time of the call while staying close to NSubstitute's existing syntax. Instead of capturing object states or requiring manual workarounds, the implementation directly solves the timing issue without compromising NSubstitute's simple and intuitive API style.

Let me know if you'd like me to make any adjustments to the implementation or tests.

loop8ack avatar Jan 30 '25 22:01 loop8ack

I discovered a significant limitation with the current implementation: Because it uses IQuery internally, which takes precedence over other routes in NSubstitute's routing system, any mock configuration set up before WillReceive.InOrder() won't be applied to calls within WhileExecuting(). For example:

// This configuration won't affect calls inside WhileExecuting
action.When(x => x.SomeMethod()).Do(_ => /* ... */);

WillReceive
    .InOrder(() => /* ... */)
    .WhileExecuting(() =>
    {
        // The configured callback won't be executed here
        action.SomeMethod();
    });

This is a significant limitation as it prevents using configured mocks together with this new feature. I currently don't have a solution for this and would appreciate suggestions on how to properly integrate with NSubstitute's routing system.

loop8ack avatar Feb 02 '25 18:02 loop8ack

Had a quick look at this but haven't come up with a good answer sorry. :( Not sure if it is possible, but maybe look at running the expectation building in query, and run the actions themselves using normal nsub routing but adjust the call handling to check with the expectations first? (so it functions more like a standard .Received call)

disclaimer: no idea if this would work, just one idea to look at if you're stuck

dtchepak avatar Mar 23 '25 05:03 dtchepak