NSubstitute icon indicating copy to clipboard operation
NSubstitute copied to clipboard

Arg.IsEquivalentTo to make it easier to verify objects passed to methods

Open egil opened this issue 4 years ago • 8 comments

When mocking e.g. a repository with e.g. a Store<T>(T entity) method, it would be nice to be able to write code like this, to verify that Store was called with the expected entity:

// arrange
var repository = Substitute.For<IRepository>();

// act 
// ... interact with SUT

// assert
var expected = new Entity { /* set properties */ };
repository.Received(1).Store(Arg.IsEquivalentTo(expected));

The IsEquivalentTo should ideally work similar to its namesake in FluentAssertions, the BeEquivalentTo method, described here: https://fluentassertions.com/objectgraphs/

If there is an way to achieve this by e.g. extending NSubstitute locally, I will be happy to do so, but needs some guidance on how to do it.

egil avatar Dec 17 '20 13:12 egil

Hi @egil,

Sorry for the late reply!

There was some work started on this a long time ago with IArgumentMatcher and IDescribeNonMatches, but it has never been finished. Some of the API hooks may be slightly different now, but you can see an example here: https://github.com/nsubstitute/NSubstitute/issues/160#issuecomment-53013051

If you can't get it working please let me know and I'll try to provide some guidance (in a timely manner this time 🤦. Sorry!)

dtchepak avatar Jan 11 '21 00:01 dtchepak

Hi @dtchepak,

No apologies necessary. As long as open source doesn't pay the bills, other work takes precedence. I know all too well from my open source projects.

I would love a deep integration ala Arg.IsEquivalentTo(...). Any chance you can enable that, without taking a dependency on FluentAssertions, which I assume you are not keen to do?

egil avatar Jan 11 '21 08:01 egil

Thanks for understanding 👍

IIRC, when looking at this before I kept running into edge cases. I think the ideal approach would be to make it easy to add matchers, so people can take a dependency on whatever assertion library they normally use, and hook that up to NSubstitute. The addition of records to C# 9 may also reduce the need for this feature.

@zvirja @alexandrnikitin Any objections to this feature on principle if someone wants to contribute it?

dtchepak avatar Jan 11 '21 09:01 dtchepak

I think the ideal approach would be to make it easy to add matchers, so people can take a dependency on whatever assertion library they normally use, and hook that up to NSubstitute.

I think that is a good middle ground. So basically it would be something like Arg.Check, where Check is a type that we can extend through extensions methods. That would make it possible for me to write something like Arg.Check.IsEquivalentTo(...).

The addition of records to C# 9 may also reduce the need for this feature.

Not sure I follow? If the expected argument is a record, then yes, Arg.Is(expected) should work fine.

egil avatar Jan 11 '21 09:01 egil

So basically it would be something like Arg.Check, where Check is a type that we can extend through extensions methods. That would make it possible for me to write something like Arg.Check.IsEquivalentTo(...).

Yes, I'm still not sure on a good API for extending this. Could also import static members from somewhere to have ArgEquivalentTo(...) available.

The addition of records to C# 9 may also reduce the need for this feature.

Not sure I follow? If the expected argument is a record, then yes, Arg.Is(expected) should work fine.

Yes I mean if records start to be used for data where field/property equivalence dictates value equivalence (which doesn't always make sense for objects in general) then it could reduce the need for additional support from NSubstitute.

dtchepak avatar Jan 12 '21 02:01 dtchepak

If you want to allow users to use their normal assertion libs, an approach could be to have a method that takes an Action, which, if it runs without throwing, is an indication that the arg passed, and if it throws, you capture the assertion exception error message and wrap it in an appropriate NSubstitute exception. Could just be an overload to Is or perhaps a new one named Passes. E.g.:

//​ assert​
​var​ ​expected​ ​=​ ​new​ ​Entity​ { ​/*​ set properties ​*/​ };
​repository​.​Received​(​1​).​Store​(​Arg​.​Is(​x => x.Should().BeEquivalentTol(expected)));

A bit more wordy than a native builtin IsEquivalentTo, but it is a good compromise.

egil avatar Jan 12 '21 07:01 egil

@egil A while ago I solved this issue for myself. I put it up as a nuget, too. See https://github.com/ModernRonin/NSubstitute.Equivalency

This is simply an extension of NSubstitute that uses FluentAssertions and exposes ArgEx.IsEquivalentTo(...).

While I solved only 4 cases that I need often, they cover the vast majority of equivalency matching.

ModernRonin avatar Mar 09 '22 19:03 ModernRonin

Just thought I'd share the approach I've taken which avoids needing an extension method and along the lines of the original post. I've combined the Arg.Do method with FluentAssertions to extract the argument being passed to a mocked method and then asserting the equivalency with FluentAssertions .Should().BeEquivalentTo(). It's a little wordy, but haven't needed to use it often

[Fact()]
public async Task Test()
{
    var tableClient = Substitute.For<TableClient>();

    MyEntity entity = null;
    tableClient.UpsertEntityAsync(Arg.Do<MyEntity>(x => entity = x), TableUpdateMode.Replace);

    var myService = new MyService(tableClient);
    await myService.ProcessDataAsync();

    config.Should()
        .BeEquivalentTo(new MyEntity
        {
            PartitionKey = nameof(MyEntity),
            RowKey = nameof(MyEntity),
            ETag = ETag.All,
            Date = DateTime.SpecifyKind(DateTime.Today, DateTimeKind.Utc)
        });
}

milkyware avatar Sep 14 '23 00:09 milkyware