Возврат результата метода, который возвращает другую замену, выдает исключение в NSubstitute

У меня возникла странная проблема при использовании NSubstitute несколько раз, и хотя я знаю, как обойти это, я никогда не мог объяснить это.

Я разработал то, что, как представляется, является минимально необходимым для доказательства проблемы, и похоже, что это связано с использованием метода для создания замещенного возвращаемого значения.

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;
}

Когда я запускаю вышеуказанный тест, я получаю следующее исключение:

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.

Однако, если я изменю метод проверки, чтобы вернуть это:

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

или это:

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

Он работает.

Мне просто интересно, может ли кто-нибудь объяснить, почему это происходит?

Ответ 1

Чтобы заставить синтаксис NSubstitute работать, происходит некоторая беспорядочность, происходящая за кулисами. Это один из тех случаев, когда он нас кусает. Сначала рассмотрим измененную версию вашего примера:

sub.MyProperty.Returns(someValue);

Сначала вызывается sub.MyProperty, который возвращает IMyObject. Затем вызывается метод расширения Returns, который должен каким-то образом решить, какой вызов ему нужно вернуть someValue для. Для этого NSubstitute записывает последний вызов, который он получил в каком-либо глобальном состоянии. Returns в псевдоиш-коде выглядит примерно так:

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
}

Итак, оценка всего вызова выглядит примерно так:

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

Теперь посмотрим, что произойдет, когда мы вызываем другую замену при попытке установить возвращаемое значение:

sub.MyProperty.Returns(MyMethod());

Снова это оценивает sub.MyProperty, тогда необходимо оценить Returns. Прежде чем он сможет это сделать, ему необходимо оценить аргументы Returns, что означает запуск MyMethod(). Эта оценка выглядит следующим образом:

//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!*

Есть еще один пример проблем, которые могут вызвать здесь.

Вы можете обойти это, отложив вызов до MyMethod(), используя:

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

Это работает, потому что MyMethod() будет выполняться только тогда, когда ему нужно использовать возвращаемое значение, поэтому статический метод GetLastSubstituteCalled не запутается.

Вместо того, чтобы делать это, я предпочитаю избегать других вызовов заменителей, пока я занят настройкой.

Надеюсь, это поможет.:)