ronaldbosma/FluentAssertions.ArgumentMatchers.Moq

Its.EquivalentTo fails silently when matching an object of descendant type

Closed this issue · 7 comments

xzxzxc commented

Consider an example

public record SomeType(int i);

public interface IDependency
{
  void DoSomething(IEnumerable<SomeType> _);
}

[Fact]
public void Test()
{
  var mock = new Mock<IDependency>();
  var list = new List<SomeType> { new SomeType(1) };
  
  mock.Object.DoSomething(list.ToArray());
	
  mock.Verify(x => x.DoSomething(Its.EquivalentTo(list)));
}

The test will fail with an exception:

Moq.MockException
Expected invocation on the mock at least once, but was never performed: x => x.DoSomething(Its.EquivalentTo<List>([SomeType { i = 1 }]))
Performed invocations:
MockIDependency:1 (x):
IDependency.DoSomething([SomeType { i = 1 }])
at Moq.Mock.Verify(Mock mock, LambdaExpression expression, Times times, String failMessage) in ...

To fix the test, I have to specify the exact argument type for the DoSomething method

mock.Verify(x => x.DoSomething(Its.EquivalentTo<IEnumerable<SomeType>>(list)));

It's impossible to understand what's wrong from the exception message.

Would you please clarify exception messages for such cases or add support for descendant argument types matching so the tests in the example will not fail?

Tested with net7.0, Moq v4.18.4, FluentAssertions.ArgumentMatchers.Moq v2.0.0.

Hello @xzxzxc.

This package combines Moq and FluentAssertions and relies on there logic and error messages. The error message you're getting comes from Moq.

The following test only uses Moq logic and results in the same error.

[TestMethod]
public void TestMethod3()
{
    var mock = new Mock<IDependency>();
    var list = new List<SomeType> { new SomeType(1) };

    mock.Object.DoSomething(list.ToArray());

    mock.Verify(x => x.DoSomething(It.IsAny<List<SomeType>>()));
}

It can be fixed in a similar way as your test.

mock.Verify(x => x.DoSomething(It.IsAny<IEnumerable<SomeType>>()));

Since there is now way of knowing in the Its.EquivalentTo method that IEnumerable<SomeType> is expected instead of List<SomeType>, the issue can only be addressed in the Moq library.

xzxzxc commented

Hello @ronaldbosma. Thank you for the answer!

Do I understand correctly that the test in my example does not work because we expect the argument to be of a type that the compiler chose for the EquivalentTo<T>() method?

If yes, can we change this behavior to check only the equivalency without the type?

I'm asking because when I write mock.Verify(x => x.DoSomething(It.IsAny<List<SomeType>>())); it's clear that I what to assert the argument's type, but when I write mock.Verify(x => x.DoSomething(Its.EquivalentTo(list))); it's unclear.

Yes. The type is inferred by the compiler. So, the Its class only knows about List<SomeType>. It doesn't know that IEnumerable<SomeType> is expected.

The Its.EquivalentTo method uses the Match.Create method of Moq to configure what comparison method (Its.AreEquivalent) to execute, as you can see here. The Match.Create returns a type of List<SomeType>.

In your example, the Its.AreEquivalent method (that performs the comparison using FluentAssertion) is never called by Moq. So we can't do anything there to ignore the type difference, because it's never called.

I've created branch issue/7 to show the problem. If you put a breakpoint on line 50 of the Its class (see comment //TODO: add breakpoint) and execute the tests in Issue7Tests.cs, you'll see that the Its.AreEquivalent method is not called in your case. Only when you specify IEnumerable<SomeType> on the Its.EquivalentTo method or pass in an argument of type IEnumerable<SomeType> is the method called.

As I mentioned before. I think this is an issue might have to be addressed by Moq.

xzxzxc commented

Thank you!
I'll take a look a bit later. Maybe, I'll come up with some ideas to avoid this check.

xzxzxc commented

I found a way of fixing my test with the help of the newer Moq library version. Created a PR.

@xzxzxc Thanks for your contribution. I've merged the change in PR #10.

I've decided to make this a major version change. It is not a breaking change at compile time, but the runtime behavior has changed. Although I doubt there are people who rely on the fact that the verification on the mock fails if an instance of List<SomeType> is passed where IEnumerable<SomeType> is expected, you never know.

You can update to version 3.0.0 to get the change.

xzxzxc commented

@ronaldbosma Super cool! Thank you for your help and such quick feedback!