nsubstitute/NSubstitute

ReceivedCallsException despite the expected and actual calls being identical.

hgirma opened this issue · 4 comments

Describe the bug
I encountered a failing test with a ReceivedCallsException despite the expected and actual calls being identical.

    NSubstitute.Exceptions.ReceivedCallsException : Expected to receive exactly 1 call matching:
	AddAsync({"id":0,"transactionKind":8,"userId":"1","opportunityId":0,"opportunityType":"Redeem","currencyId":0,"currencyType":null,"amount":-10.0,"note":"Redeemed: ","createdBy":"1","createdDate":"2024-05-04T10:59:45.7847177-04:00","isDeleted":false,"user":null})
Actually received no matching calls.
Received 1 non-matching call (non-matching arguments indicated with '*' characters):
	AddAsync(*{"id":0,"transactionKind":8,"userId":"1","opportunityId":0,"opportunityType":"Redeem","currencyId":0,"currencyType":null,"amount":-10.0,"note":"Redeemed: ","createdBy":"1","createdDate":"2024-05-04T10:59:45.7847177-04:00","isDeleted":false,"user":null}*)

I don't see the difference between actual and expected.

if I were to assert
myService.Received(1).AddAsync(Arg.Is(t => t.Amout == -10 && t.UserId = "1" & etc ...) and compare each item, it passes

however it fails when doing the below
myService.Received(1).AddAsync(transactionObject);

To Reproduce
I am not sure what to make of this or how to troubleshoot further, any ideas?

Expected behaviour
should pass validation as the expect and actual are identical
myService.Received(1).AddAsync(transactionObject);
as oppose to
myService.Received(1).AddAsync(Arg.Is(t => t.Amout == -10 && t.UserId = "1" & etc ...) comparing each propery with &&

Environment:

  • NSubstitute version: 5.1.0
  • NSubstitute.Analyzers version: CSharp 1.0.17
  • Platform: .net 8

Additional context
I used TimeProvider and FakeTimeProvider to make sure the date/times are identical.

Hi @hgirma ,

Is equality implemented for this type? Or is it using reference equality?
e.g. if you new up two instances and Assert.That(a, Is.EqualTo(b)) does it pass when NSubstitute is not involved?

Hi @dtchepak

No it does not, I created a sample repo to reproduce the issue

https://github.com/hgirma/WebApplication2

similar setup work with other types in my codebase and without IEquatable implemented.

I also waited for an example, because I expected the same. Many of our junior developers make the same mistake. We humans always think that 2 objects having the same information must be the same. Sadly that is not what .NET does. If you dont change anything about your objects, then Equals only returns true if they are the same object.

This has nothing to do with NSubstitute, but is how .NET works:

      // Assert
      user.Should().Be(user);
      transaction.Should().Be(new Transaction
      {
        Amount = -shopItem.Amount,
        CreatedBy = user.Id,
        CurrencyId = shopItem.CostCurrencyTypeId,
        CurrencyType = shopItem.CostCurrencyType,
        Note = $"Redeemed: {shopItem.Description}",
        UserId = user.Id,
        OpportunityType = "Redeem",
        TransactionKind = TransactionKind.Shop,
        CreatedDate = _timeProvider.GetUtcNow().LocalDateTime,
        IsDeleted = false,
      });

This is basically the same test and fails exactly the same way.

There are many ways to solve this:

  • Write a predicate comparing property for property, like you did in your working test
  • Let your classes implement IEquitable, basically overriding Equals
  • Use records where the SourceGenerator does this for you
  • Combine NSubstitute with FluentAssertions and use BeEquivalentTo instead of Be

yep, junior mistake :) I went with predicate for now...