【问题标题】:How do I get NSubstitute mocks to fail when the arguments don't match the given pattern?当参数与给定模式不匹配时,如何让 NSubstitute 模拟失败?
【发布时间】:2021-07-29 02:02:21
【问题描述】:

我负责测试我的团队正在维护的用 C 和 C# 开发的遗留软件。最初的团队使用 NSubstitute 3.1 为代表创建测试替身,以便对 C# 部分的 API 执行单元测试。这是一个这样的测试替身,省略了不相关的细节:

private static byte[] MockSelectByAidWithoutData(ushort retVal)
{
    var expectedIn= "FFFEFDFCFB".HexToBytes();
    var expectedOut= "010203040506070809".HexToBytes();

    var fake = Substitute.For<SomeDelegate>();
    fake(Arg.Is<byte[]>(x => expectedIn.SequenceEqual(x.Take(expectedIn.Length))),
            Arg.Is(0x00),
            Arg.Is(expectedIn.Length),
            Arg.Any<int>(),
            Arg.Any<int>(),
            out int outputLength)
        .Returns(x =>
            {
                expectedOut.CopyTo((Array)x[0], 0);
                x[5] = expectedOut.Length;
                return retVal;
            }
        );
    Mediator.GetInstance().Delegate = fake;
    return expectedOut;
}

现在,如果使用与 fake() 调用中指定的参数匹配的参数调用假委托,它会返回 retVal 值,每个人都很高兴。但是,如果某些值不匹配,则返回零。由于零是一个有效但不正确的值,因此继续执行,我得到一个错误,这不是我正在测试的问题的根本原因(即当问题实际上是错误输入时输出错误)

我正在寻找一种方法:

  • 为不符合预期的值指定“包罗万象”行为,或
  • 如果参数与预期不符,则会出现异常

这样测试用例会在接收到带有有意义消息的错误输入时立即失败,并且不会触发只会污染测试结果的进一步行为。

提前致谢,

德克

附:如果真的有必要,我可能可以安全地切换到更新版本的 NSubstitute。

【问题讨论】:

  • 我认为您需要使用 Received 来断言使用特定参数进行了调用。如果这将复制来自 Returns 存根的逻辑,您可能会使用 ReturnsForAnyArgs 进行切换,并且仅匹配 Received 断言中的参数。
  • 这类似于我正在使用的解决方法,我只接受对 fake() 的调用中的任何参数,然后使用 AndDoes 在回调中手动执行所有断言。 (Received 部分永远不会执行,因为 retval = 0 会触发异常。)但是我不太喜欢这种解决方法:我正在手动执行框架应该为我做的事情。似乎我只是使用了错误的工具来完成这项工作。

标签: c# unit-testing mocking nunit nsubstitute


【解决方案1】:

为不符合预期的值指定“全部”行为

我想我已经找到了一种方法可以做到这一点。如果您首先为所有参数存根“全部捕获”/失败案例,那么您可以存根更具体的调用。 NSubstitute 将尝试匹配提供的最新规范,回退到较早的存根值。

这是一个示例。

注意它使用了 NSubstitute 4.x 中引入的 NSubstitute.Extensions 命名空间中的 Configure。这不是绝对必要的,因为如果您使用参数匹配器,NSubstitute 会自动假定您正在配置调用,但是在配置这样的重叠调用时使用它是一个很好的模式。

using NSubstitute;
using NSubstitute.Extensions; // required for Configure()

public class Thing {
    public string Id { get; set; }
}

public interface ISample {
    int Example(Thing a, string b);
}

public class UnexpectedCallException : Exception { }

[Fact]
public void ExampleOfStubOneCallButFailOthers() {
    var sub = Substitute.For<ISample>();

    // Catch all case:
    sub.Example(null, null).ReturnsForAnyArgs(x => throw new UnexpectedCallException());

    // Specific case. We use Configure from NSubstitute.Extensions to
    // be able to stub this without getting an UnexpectedCallException.
    // Not strictly necessary here as we're using argument matchers so NSub
    // already knows we're configuring a call, but it's a good habit to get into.
    // See: https://nsubstitute.github.io/help/configure/
    sub.Configure()
        .Example(Arg.Is<Thing>(x => x.Id == "abc"), Arg.Any<string>())
        .Returns(x => 42);

    // Example of non-matching call:
    Assert.Throws<UnexpectedCallException>(() =>
        sub.Example(new Thing { Id = "def" }, "hi")
    );

    // Example of matching call:
    Assert.Equal(42, sub.Example(new Thing { Id = "abc" }, "hello"));
}

您可以扩展它以包含有关不匹配参数的信息,但这将是一些自定义工作。如果您查看 NSubstitute 的一些参数格式化代码,这些代码可能可重用以帮助解决此问题。


更新以包含委托示例

我只是用一个委托来运行它,它也通过了:

public delegate int SomeDelegate(Thing a, string b);

[Fact]
public void ExampleOfStubOneDelegateCallButFailOthers() {
    var sub = Substitute.For<SomeDelegate>();
    sub(null, null).ReturnsForAnyArgs(x => throw new UnexpectedCallException());
    sub.Configure()
        .Invoke(Arg.Is<Thing>(x => x.Id == "abc"), Arg.Any<string>())
        .Returns(x => 42);
    Assert.Throws<UnexpectedCallException>(() => sub(new Thing { Id = "def" }, "hi"));
    Assert.Equal(42, sub(new Thing { Id = "abc" }, "hello"));
}

【讨论】:

  • 谢谢大卫,这就是我所说的,但它似乎不适用于委托,甚至不应该工作,因为 Configure() 方法只存在于接口/类级别。
  • @DekDekku 我用一个似乎可以正常工作的代表示例更新了答案。您能描述一下您在委托示例中看到的错误/行为吗?
  • 再次感谢。正如我所说,Configure() 方法在假对象上不可用。我会再试一次,因为我是从 v3.1 更新的,也许 Intellisense 需要更多时间来刷新。
  • 完美!这是 v4 的一个新特性,因此我的项目中缺少它(只是写这个来获得所需数量的字符......)
  • Configure() 在这种情况下不是严格要求的。如果您使用了参数匹配器,NSubstitute 将假定您正在配置调用。所以这应该在没有 Configure() 调用的情况下在 v3 中工作。 (我在答案中添加了一个注释。)感谢您指出这一点!
猜你喜欢
  • 2018-09-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多