NSubstitute icon indicating copy to clipboard operation
NSubstitute copied to clipboard

Retrieving arguments from a call

Open jgauffin opened this issue 10 years ago • 18 comments

Sometimes it's not possible to specify arguments directly in a call. Instead I have to be able to retrieve them for assertions.

I currently do that by using

var arg = (IEnumerable<int>) repos.ReceivedCalls().First(x => x.GetMethodInfo().Name == "Save").GetArguments()[0];

However, that's a bit messy. It would be nice with a simpler syntax like:

var arg = (IEnumerable<int>)repos.ReceivedArgumentsFor("Save")[0];

jgauffin avatar Aug 18 '14 13:08 jgauffin

Hi @jgauffin,

Do you mean you want to get the arguments for multiple overloads of a call? Or just capture an argument value in general?

Does Arg.Do cover what you need? It translates to "do something with this argument", like storing its value for example:

IEnumerable<int> arg = null;
repos.Save(Arg.Do<IEnumerable<int>>(x => arg = x), Arg.Any<SaveOption>());
/* invoke subject under test */
Assert.That(arg, HasAllTheRequiredThingoes());

(Can also use Arg.Do<Blah>(argsUsed.Add) if you want to store the arg used for each call in a list or similar)

dtchepak avatar Aug 19 '14 13:08 dtchepak

No, I want to assert that the argument contains a specific value.

var userNames = (IEnumerable<int>)repos.ReceivedArgumentsFor("Save")[0];
userNames.Should().Contain(42);

It could also have been solved if you used equality comparer for all elements in an enumerable:

repos.Received().Save(new []{ 42 });

That code will fail today as the specified list is not the same reference as the real invocation.

jgauffin avatar Aug 19 '14 13:08 jgauffin

Sorry if I'm misunderstanding, but wouldn't the following work?

//Arrange
IEnumerable<int> userNames = null;
repos.Save(Arg.Do<int>(x => userNames = x));
//Act
subject.DoStuff();
//Assert
userNames.Should().Contain(42);

dtchepak avatar Aug 19 '14 13:08 dtchepak

Better to add a complete sample :)

[TestClass]
public class DemoTests
{
    [TestMethod]
    public void Show_what_I_meant_to_test()
    {
        var repos = Substitute.For<IRepos>();

        var sut = new SomeService(repos);
        sut.Process();

        repos.Received().Save(new int[] {1, 2, 3});
    }
}

public interface IRepos
{
    void Save(IEnumerable<int> ids);
}

internal class SomeService
{
    private readonly IRepos _repos;

    public SomeService(IRepos repos)
    {
        _repos = repos;
    }

    public void Process()
    {
        _repos.Save(new[] {1, 2, 3});
    }
}

As you can see, I want to make sure that the service invokes the method with correct arguments. But as I don't have access to the generated list I cannot use it for comparison.

Edit: Your code works. But it's not as clean as the rest of the API. The cleanness is one of the awesome things with NSubtitute and one of the reasons that we've switched from Moq to Nsubstitute. Your example code feels more like Moq.

jgauffin avatar Aug 19 '14 13:08 jgauffin

My first preference for testing this would be to use a custom argument matcher:

[Test]
public void UsingArgMatcher() {
    var repos = Substitute.For<IRepos>();

    var sut = new SomeService(repos);
    sut.Process();

    repos.Received().Save(Arg.Is(EquivalentTo(new int[] { 1, 2, 3 })));
}

private Expression<Predicate<IEnumerable<T>>> EquivalentTo<T>(IEnumerable<T> enumerable) {
    return x => Equiv(enumerable, x);
}

private bool Equiv<T>(IEnumerable<T> a, IEnumerable<T> b) { ... }

This has the problem that the failure messages are absolutely terrible. I had implemented a custom arg matcher protocol to fix this, but it was suggested that I should leave it as an Expression and do some work inspecting the expression and creating a decent failure message from that. And I never got around to it. :-\

That leaves us with the messy Arg.Do thing, and use an assertion library to get a decent error message:

[Test]
public void UsingArgCatcher()
{
    IEnumerable<int> savedValues = null;
    var repos = Substitute.For<IRepos>();
    repos.Save(Arg.Do<IEnumerable<int>>(x => savedValues = x));

    var sut = new SomeService(repos);
    sut.Process();

    Assert.That(savedValues, Is.EquivalentTo(new[] {1, 2, 3}));
}

You're right this should be a lot better, but it's something I've needed (and has been requested) so rarely that I've never quite felt justified investing the time in it.

dtchepak avatar Aug 20 '14 00:08 dtchepak

I've implemented this using ArgumentMatcher and FluentAssertions to check the equivalency.

This is how I did it:

using System;
using FluentAssertions;
using FluentAssertions.Equivalency;

namespace NSubstitute.Core.Arguments
{
    public class EquivalentArgumentMatcher<T> : IArgumentMatcher, IDescribeNonMatches
    {
        private static readonly ArgumentFormatter DefaultArgumentFormatter = new ArgumentFormatter();
        private readonly object _expected;
        private readonly Func<EquivalencyAssertionOptions<T>, EquivalencyAssertionOptions<T>> _options;

        public EquivalentArgumentMatcher(object expected)
            : this(expected, x => x.IncludingAllDeclaredProperties())
        {
        }

        public EquivalentArgumentMatcher(object expected, Func<EquivalencyAssertionOptions<T>, EquivalencyAssertionOptions<T>> options)
        {
            _expected = expected;
            _options = options;
        }

        public override string ToString()
        {
            return DefaultArgumentFormatter.Format(_expected, false);
        }

        public string DescribeFor(object argument)
        {
            try
            {
                ((T)argument).ShouldBeEquivalentTo(_expected, _options);
                return string.Empty;
            }
            catch (Exception ex)
            {
                return ex.Message;
            }
        }

        public bool IsSatisfiedBy(object argument)
        {
            try
            {
                ((T)argument).ShouldBeEquivalentTo(_expected, _options);
                return true;
            }
            catch
            {
                return false;
            }
        }
    }
}
using System;
using FluentAssertions.Equivalency;
using NSubstitute.Core;
using NSubstitute.Core.Arguments;

namespace NSubstitute
{
    public static class NSubstituteExtensions
    {
        public static T Equivalent<T>(this object obj)
        {
            SubstitutionContext.Current.EnqueueArgumentSpecification(new ArgumentSpecification(typeof(T), new EquivalentArgumentMatcher<T>(obj)));
            return default(T);
        }

        public static T Equivalent<T>(this T obj)
        {
            return Equivalent<T>((object)obj);
        }

        public static T Equivalent<T>(this object obj, Func<EquivalencyAssertionOptions<T>, EquivalencyAssertionOptions<T>> options)
        {
            SubstitutionContext.Current.EnqueueArgumentSpecification(new ArgumentSpecification(typeof(T), new EquivalentArgumentMatcher<T>(obj, options)));
            return default(T);
        }

        public static T Equivalent<T>(this T obj, Func<EquivalencyAssertionOptions<T>, EquivalencyAssertionOptions<T>> options)
        {
            return Equivalent((object)obj, options);
        }
    }
}

And this is how I'd use it:

[TestClass]
public class DemoTests
{
    [TestMethod]
    public void Show_what_I_meant_to_test()
    {
        var repos = Substitute.For<IRepos>();

        var sut = new SomeService(repos);
        sut.Process();

        repos.Received().Save(new int[] {1, 2, 3}.Equivalent());
    }
}

mrinaldi avatar Aug 22 '14 01:08 mrinaldi

This is really cool! Thanks for sharing the code.

I was originally of thinking of having something like an Arg.Matches(IArgumentMatcher<T>) method (or Arg.Is( IArgumentMatcher<T> ) overload?), or Arg.Matches property that returned something people could hang their own extension methods off. (e.g. Arg.Matches.Equivalent(new [] {1,2,3}), although that's horribly verbose). (Would have to cater for non-generic IArgumentMatcher too.)

If you've got any ideas about convenient ways for people to add extensions like this please let me know. I still like this idea of using expression trees but in the meantime I think we may be able to get something immediately useful based on your example.

dtchepak avatar Aug 25 '14 12:08 dtchepak

You could make Arg class non-static. Then I'd change the code to make the Equivalent methods an extension method of Arg:

namespace NSubstitute
{
    public static class NSubstituteExtensions
    {
        public static T Equivalent<T>(this Arg arg, object obj)
        {
            SubstitutionContext.Current.EnqueueArgumentSpecification(new ArgumentSpecification(typeof(T), new EquivalentArgumentMatcher<T>(obj)));
            return default(T);
        }

        public static T Equivalent<T>(this Arg arg, T obj)
        {
            return Equivalent<T>(arg, (object)obj);
        }

        public static T Equivalent<T>(this Arg arg, object obj, Func<EquivalencyAssertionOptions<T>, EquivalencyAssertionOptions<T>> options)
        {
            SubstitutionContext.Current.EnqueueArgumentSpecification(new ArgumentSpecification(typeof(T), new EquivalentArgumentMatcher<T>(obj, options)));
            return default(T);
        }

        public static T Equivalent<T>(this Arg arg, T obj, Func<EquivalencyAssertionOptions<T>, EquivalencyAssertionOptions<T>> options)
        {
            return Equivalent(arg, (object)obj, options);
        }
    }
}

Then I could call it like this:

[TestClass]
public class DemoTests
{
    [TestMethod]
    public void Show_what_I_meant_to_test()
    {
        var repos = Substitute.For<IRepos>();

        var sut = new SomeService(repos);
        sut.Process();

        repos.Received().Save(Arg.Equivalent(new int[] {1, 2, 3}));
    }
}

mrinaldi avatar Sep 02 '14 12:09 mrinaldi

Wouldn't you still need an instance to call the extension method?

repos.Received().Save(new Arg().Equivalent(new int[] {1, 2, 3}));

dtchepak avatar Sep 02 '14 13:09 dtchepak

Yeah, definitely. I just woke up so I guess I wasn't thinking straight :)

I don't really like Arg.Matches.Equivalent(new [] {1,2,3}), but I guess you can't get any less verbose than this. You could also create a new Is() method, it would seem less strange, but you'd have to make it a method, not a property because of the already existing Is methods. Arg.Is().Equivalent(new [] {1,2,3}) seems more fluid, but verbose yet.

mrinaldi avatar Sep 02 '14 13:09 mrinaldi

Here are my alternatives:

Usage:

var actual = processor.GetListArgument<OutboundMessageContainer>("Process", 0);
actual[0].Should().BeSameAs(expected);

Code:

public static class Extensions
{
    public static object GetArgument(this object instance, string methodName, string argumentName)
    {
        //var args=  instance.Get
        var call = instance.ReceivedCalls().FirstOrDefault(x => x.GetMethodInfo().Name == methodName);
        if (call == null)
            throw new InvalidOperationException(string.Format("Did not find a method named '{0}'.", methodName));

        var parameters = call.GetMethodInfo().GetParameters();
        for (int i = 0; i < parameters.Length; i++)
        {
            var p = parameters[i];
            if (p.Name == argumentName)
                return call.GetArguments()[i];
        }

        throw new InvalidOperationException(string.Format("Did not find argument '{0}' in method '{1}'.", argumentName, methodName));
    }

    public static object GetArgument(this object instance, string methodName, int argumentIndex)
    {
        //var args=  instance.Get
        var call = instance.ReceivedCalls().FirstOrDefault(x => x.GetMethodInfo().Name == methodName);
        if (call == null)
            throw new InvalidOperationException(string.Format("Did not find a method named '{0}'.", methodName));

        return call.GetArguments()[argumentIndex];
    }

    public static T GetArgument<T>(this object instance, string methodName, int argumentIndex)
    {
        //var args=  instance.Get
        var call = instance.ReceivedCalls().FirstOrDefault(x => x.GetMethodInfo().Name == methodName);
        if (call == null)
            throw new InvalidOperationException(string.Format("Did not find a method named '{0}'.", methodName));

        return (T)call.GetArguments()[argumentIndex];
    }

    public static List<T> GetListArgument<T>(this object instance, string methodName, int argumentIndex)
    {
        //var args=  instance.Get
        var call = instance.ReceivedCalls().FirstOrDefault(x => x.GetMethodInfo().Name == methodName);
        if (call == null)
            throw new InvalidOperationException(string.Format("Did not find a method named '{0}'.", methodName));

        return ((IEnumerable<T>) call.GetArguments()[argumentIndex]).ToList();
    }
}

jgauffin avatar Sep 03 '14 16:09 jgauffin

As a first step Arg.Matches(IArgumentMatcher<T>) or Arg.Is(IArgumentMatcher<T>) would be a great enabler. Especially since this is already supported but hidden from the public API as dtchepak mentioned.

I've in the mean time added access to the ArgSpecQueue using reflection in my own code to use custom IArgumentMatcher<T>'s.

As for the verbosity, I extract the Arg into a another method to reduce the syntax noise anyway. As demonstrated above.

public void UsingArgMatcher() {
    var repos = Substitute.For<IRepos>();

    var sut = new SomeService(repos);
    sut.Process();

    repos.Received().Save(Arg.Is(EquivalentTo(new int[] { 1, 2, 3 })));
}

private IArgumentMatcher<IEnumerable<T>> EquivalentTo<T>(IEnumerable<T> enumerable) {
    return ...;
}

chrisjansson avatar Feb 12 '15 11:02 chrisjansson

I'm testing some stuff that uses RestSharp and I have to do some extra digging to verify the request was setup correctly. At this time I'm doing something like this:

var requestReceived = (RestRequest)client.ReceivedCalls()
    .First(x => x.GetMethodInfo().Name == "Execute").GetArguments()[0];

Assert.True(
    requestReceived.Parameters.Any(
    x => x.Type == ParameterType.UrlSegment 
        && x.Name == "envelopeid" 
        && x.Value == envelopeId));

I'm new to using NSubstitute so I'm not sure how well custom argument matcher would work in my case, if at all. It would be nice to have something a little more succinct to extract the requestReceived argument from a called method.

jholland918 avatar Apr 08 '15 06:04 jholland918

Hi @jholland918, You could try something like:

client.Received().Execute(Arg.Is(x => x.Type == ParameterType.UrlSegment && ... ) );

Or capture the argument (either with a single variable, or with a list, depending on how many calls are being received) and assert on it later:

ArgType arg = null;
client.Execute(Arg.Do(x => arg = x));
// ... rest of test ...
Assert.True(arg.Type == ParameterType.UrlSegment && ... );

dtchepak avatar Apr 09 '15 22:04 dtchepak

This works for me, thanks!

jholland918 avatar Apr 16 '15 14:04 jholland918

@mrinaldi have you tried your approach with DidNotReceive ?

I gave your approach a try on my test project and it worked well with Received but it did not work as expected with DidNotReceive

lukefan6 avatar Nov 16 '16 05:11 lukefan6

@dtchepak,

I'm having an issue with Arg.Is(T value) where the default equality comparer is not working as I'd expect. I've got a class that inherits from a base class to provide equality, get hash code and the like, but when used with an Argument matcher, equality fails. Equality works as expected, if I check this manually. Should I raise a separate issue with this?

SeanFarrow avatar Sep 28 '18 11:09 SeanFarrow

Hi @SeanFarrow,

There is an existing issue for NSub not using IEquatable properly, but if you've found a case where it is not working with an overridden Equals then please raise it as a new bug. Thanks!

dtchepak avatar Sep 28 '18 23:09 dtchepak