【问题标题】:Returning the result of a method that returns another substitute throws an exception in NSubstitute返回返回另一个替换的方法的结果会在 NSubstitute 中引发异常
【发布时间】:2013-05-20 06:09:32
【问题描述】:

我在使用 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);

有效。

我只是想知道是否有人可以解释为什么会发生这种情况?

【问题讨论】:

    标签: c# nsubstitute


    【解决方案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!*
    

    还有另一个可能导致问题的示例here

    您可以通过将呼叫推迟到 MyMethod() 来解决此问题,方法是:

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

    这是可行的,因为MyMethod() 只会在需要使用返回值时执行,所以静态GetLastSubstituteCalled 方法不会混淆。

    尽管如此,我更愿意在忙于配置替代者时避免调用其他替代者。

    希望这会有所帮助。 :)

    【讨论】:

    • 感谢您的详细回答。这比我预期的要复杂得多。您认为这被认为是 NHibernate 中的错误吗?根据您的回答,我了解到这是一个已知问题,而且修复起来相当复杂。
    • 这是语法选择的限制。将 NSub 更改为使用显式的本地 SubstituteFactories 可能会有所帮助(我们可以在每个测试中保留大量状态并搜索它,而不是全局状态),但不确定人们是否愿意使创建 subs 的语法复杂化。我想我更喜欢这样,但我不确定这是否是一个足够大的问题来证明在这一点上进行更改是合理的。
    • 真正的问题是代码从各个角度看起来都是正确的,因此很难诊断问题。也许更好的错误消息可能会有所帮助,但我不知道它们可能是什么。再次感谢。
    • 是的,如果出现问题,那就太糟糕了。当前的 repo HEAD 检测到这种情况并抛出描述性消息。我将尝试类似地扩展 CouldNotSetReturnException 消息。感谢您的建议。
    • 我可能不明白内部发生了什么(特别是因为它已经简化了),但是让 GetLastSubstituteCalled() 围绕堆栈旋转以跟踪调用不是更有意义吗?然后你可以简单地将它们弹出并允许我们让 ob.Value 成为顶部调用,它被弹出,然后我们仍然有 sub.MyProperty 在堆栈上。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-02-27
    • 1970-01-01
    • 2022-11-27
    • 1970-01-01
    相关资源
    最近更新 更多