nsubstitute / NSubstitute

A friendly substitute for .NET mocking libraries.
https://nsubstitute.github.io
Other
2.67k stars 263 forks source link

Retrieving arguments from a call #160

Open jgauffin opened 10 years ago

jgauffin commented 10 years ago

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];
dtchepak commented 10 years ago

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)

jgauffin commented 10 years ago

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.

dtchepak commented 10 years ago

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);
jgauffin commented 10 years ago

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.

dtchepak commented 10 years ago

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.

mrinaldi commented 10 years ago

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());
    }
}
dtchepak commented 10 years ago

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.

mrinaldi commented 10 years ago

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}));
    }
}
dtchepak commented 10 years ago

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

repos.Received().Save(new Arg().Equivalent(new int[] {1, 2, 3}));
mrinaldi commented 10 years ago

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.

jgauffin commented 10 years ago

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();
    }
}
chrisjansson commented 9 years ago

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 ...;
}
jholland918 commented 9 years ago

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.

dtchepak commented 9 years ago

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 && ... );
jholland918 commented 9 years ago

This works for me, thanks!

lukefan6 commented 7 years ago

@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

SeanFarrow commented 6 years ago

@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?

dtchepak commented 6 years ago

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!