简体   繁体   中英

How to mock `object.Equals(object obj)` for an interface using Moq

I have an interesting problem to wrap your head around. Consider some interface like this one:

public interface IMyThing
{
    int Id { get; }
}

Now I want to test code that uses this interface. Maybe something with some LINQ magic. Something like this:

public class SomeClass
{
    private IMyThing _thing;

    ...

    public bool HasThing(IEnumerable<IMyThing> things)
    {
        return things.Contains(_thing);
    }
}

I'm mocking all objects implementing IMyThing using Moq :

public static IMyThing MockMyThing(int newId)
{
    var mock = new Mock<IMyThing>();
    mock.Setup(s => s.Id).Returns(newId);
    mock.Setup(s => s.Equals(It.IsAny<object>())).Returns<object>(obj =>
    {
        if (typeof(IMyThing).IsAssignableFrom(obj.GetType()))
        {
            return ((IMyThing)obj).Id == newId;
        }

        return false;
    });

    return mock.Object;
}

Here is the thing. The above code compiles without warnings but will never work. Moq creates an interceptor for the Equals() method but it is never reached. Instead, the equals method of the object proxy is called. I'm blaming the fact that I'm mocking an interface but not a concrete class.

UPDATE: Just realised Moq doesn't even create an interceptor.

Of course I could augment the IMyThing interface like this:

public interface IMyThing : IEquatable<IMyThing>
{
    int Id { get; }
}

The LINQ operator would recognize the IEquatable<T> interface and use it.

I don't want to do this because:

  • This is only usable with other IMyThing objects
  • IEquatable<T> was not meant for this purpose
  • I don't want to taint my Model only to make it mockable

How would you solve this?

I ended up contributing code to the Moq project on Github (see issue #248 ). With this changes it is possible to mock object.Equals(object obj) , object.GetHashCode() and object.ToString() , even for interface mocks.

Lets see if it gets accepted.

I think the problem comes from the fact that the Equals method is not on the interface you are mocking. If you create a class with an overridable Equals method (even if it does nothing), then you are able to mock it.

public class MyTestThing : IMyThing
{
    public virtual int Id { get; }

    public override bool Equals(object obj)
    {
        return base.Equals(obj);
    }
}

[TestCase(55)]
public void Can_mock_equals_method(int newId)
{
    var mockThing = new Mock<MyTestThing>();

    mockThing.Setup(t => t.Id).Returns(newId);
    mockThing.Setup(t => t.Equals(It.IsAny<object>()))
        .Returns<object>(t => (t as IMyThing)?.Id == newId);

    Assert.That(mockThing.Object.Equals(new MyRealThing(newId)));
}

Notice that if you comment out the Equals method on MyTestThing this test will fail, because Moq can no longer mock it.

If you are going to create test classes like this, it might be more useful to actually implement Equals fully in the class itself so you don't have to bother setting it up with Moq. You could even go a step further and create an abstract base class where the only implemented method is Equals and use this both in your tests with Moq, and derive your real implementation from it.

If the way you perform comparisons can change, you shouldn't use direct comparisons, but use objects that can do the comparisons for you.

Consider making changes to your class like this:

public class SomeClass
{
    private IMyThing _thing;

    public bool HasThing(IEnumerable<IMyThing> things, IEqualityComparer<IMyThing> comparer = null)
    {
        comparer = comparer ?? EqualityComparer<IMyThing>.Default;
        return things.Contains(_thing, comparer);
    }
}

Then you just have to mock the comparer as well.

Maybe i dont understand you fully but i dont see an extandes or overide Equals() function for IMyThing ? So i guess that my first approach will be overide, or extandes function, if it dont work i will IMyThing : IEquatable<IMyThing> , i know IEquatable<T> was not meant for this purpose but it close. and the last and I dont think that you need to do it but it exist, just create an function like bool IsEquals(IMyThing other){//check if equals}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM