简体   繁体   中英

How do I get NSubstitute mocks to fail when the arguments don't match the given pattern?

I'm responsible for the testing of a legacy software developed in C and C# that my team is maintaining. The original team used NSubstitute 3.1 to create test doubles for delegates in order to perform unit test of the APIs for the C# sections. Here's one such test double, where irrelevant details have been omitted:

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

Now, if the fake delegate is invoked with arguments that match what is specified in the fake() call, it returns the retVal value and everybody is happy. However, if some value won't match, it returns zero. Since zero is a valid but incorrect value, the execution continues and I get an error that is not the root cause of the issue I am testing (ie bad output when the problem is actually bad input)

I am looking for a way to either:

  • specify a "catch all" behaviour for the values that won't match the expectations, or
  • get an exception if the arguments don't match the expectation

so that the test case would fail immediately upon reception of the wrong input with a meaningful message and without triggering further behaviour that would just pollute the outcome of the test.

Thanks in advance,

DeK

PS I can probably switch safely to a more recent version of NSubstitute if that's really necessary.

specify a "catch all" behaviour for the values that won't match the expectations

I think I've found a way you can do this. If you first stub the "catch all" / failure case for all arguments, you can then stub more specific calls. NSubstitute will try to match the most recent specifications provided, falling back to earlier stubbed values.

Here is a sample.

Note it is using Configure from NSubstitute.Extensions namespace introduced in NSubstitute 4.x. This isn't strictly necessary because NSubstitute will automatically assume you are configuring a call if you are using argument matchers, but it is a good pattern to use when configuring overlapping calls like this.

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

You could extend this to include information about arguments that do not match, but that will be a bit of custom work. If you look at some of NSubstitute's argument formatting code that might be re-usable to help with this.


Update to include delegate example

I just ran this with a delegate instead and it also passes:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM