Returning the result of a method that returns another substitute throws an exception in NSubstitute

StackOverflow https://stackoverflow.com/questions/16643526

  •  30-05-2022
  •  | 
  •  

سؤال

I have run into a weird issue while using NSubstitute a few times and although I know how to work around it I've never been able to explain it.

I've crafted what appears to be the minimum required test to prove the problem and it appears to be something to do with using a method to create a substituted return value.

public interface IMyObject
{
    int Value { get; }
}

public interface IMyInterface
{
    IMyObject MyProperty { get; }
}

[TestMethod]
public void NSubstitute_ReturnsFromMethod_Test()
{
    var sub = Substitute.For<IMyInterface>();

    sub.MyProperty.Returns(MyMethod());
}

private IMyObject MyMethod()
{
    var ob = Substitute.For<IMyObject>();
    ob.Value.Returns(1);
    return ob;
}

When I run the above test I get the following exception:

Test method globalroam.Model.NEM.Test.ViewModel.DelayedAction_Test.NSubstitute_ReturnsFromMethod_Test threw exception: 
NSubstitute.Exceptions.CouldNotSetReturnException: Could not find a call to return from.
Make sure you called Returns() after calling your substitute (for example: mySub.SomeMethod().Returns(value)).
If you substituted for a class rather than an interface, check that the call to your substitute was on a virtual/abstract member.
Return values cannot be configured for non-virtual/non-abstract members.

However, if I change the test method to return this:

sub.MyProperty.Returns((a) => MyMethod());

or this:

var result = MyMethod();
sub.MyProperty.Returns(result);

It works.

I'm just wondering if anyone can explain why this happens?

هل كانت مفيدة؟

المحلول

To get the NSubstitute syntax to work there is some messiness going on behind the scenes. This is one of those cases where it bites us. Let's look at a modified version of your example first:

sub.MyProperty.Returns(someValue);

First, sub.MyProperty is called, which returns an IMyObject. The Returns extension method is then called, which needs to somehow work out which call it needs to return someValue for. To do this, NSubstitute records the last call it received in some global state somewhere. Returns in pseudo-ish-code looks something like this:

public static void Returns<T>(this T t, T valueToReturn) {
  var lastSubstitute = bigGlobOfStaticState.GetLastSubstituteCalled();
  lastSubstitute.SetReturnValueForLastCall(valueToReturn);
  bigGlobOfStaticState.ClearLastCall(); // have handled last call now, clear static state
}

So evaluating the entire call looks a bit like this:

sub.MyProperty         // <-- last call is sub.MyProperty
   .Returns(someValue) // <-- make sub.MyProperty return someValue and
                       //     clear last call, as we have already set
                       //     a result for it

Now let's see what happens when we call another substitute while trying to set the return value:

sub.MyProperty.Returns(MyMethod());

Again this evaluates sub.MyProperty, then needs to evaluate Returns. Before it can do that, it needs to evaluate the arguments to Returns, which means running MyMethod(). This evaluation looks more like this:

//Evaluated as:
sub.MyProperty     // <- last call is to sub.MyProperty, as before
   .Returns(
     // Now evaluate arguments to Returns:
     MyMethod()
       var ob = Substitute.For<IMyObject>()
       ob.Value      // <- last call is now to ob.Value, not sub.MyProperty!
         .Returns(1) // <- ok, ob.Value now returns 1, and we have used up the last call
     //Now finish evaluating origin Returns:
     GetLastSubstituteCalled *ugh, can't find one, crash!*

There is another example of the problems this can cause here.

You can work around this by deferring the call to MyMethod(), by using:

sub.MyProperty.Returns(x => MyMethod());

This works because MyMethod() will only execute when it needs to use a return value, so the static GetLastSubstituteCalled method doesn't get confused.

Rather than doing that though, I prefer avoiding other calls to substitutes while I am busy configuring one.

Hope this helps. :)

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top