如何使用Moq模拟一个接口的`object.Equals(object obj)`方法

10

我有一个有趣的问题需要你思考。考虑这样一个界面:

public interface IMyThing
{
    int Id { get; }
}

现在我想测试使用这个接口的代码。也许有一些LINQ的魔法。就像这样:

public class SomeClass
{
    private IMyThing _thing;

    ...

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

我正在使用Moq来模拟实现IMyThing接口的所有对象:

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;
}
这样说吧,上面的代码虽然可以编译通过,但是不会起作用。MoqEquals() 方法创建了拦截器,但从未被调用。相反,对象代理的等式方法被调用了。我认为这是因为我在模拟一个接口而不是具体类。

更新:刚意识到Moq甚至没有创建拦截器。

当然,我可以像这样增强IMyThing接口:

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

LINQ操作符将识别IEquatable<T>接口并使用它。

我不想这样做,因为:

  • 这只能与其他IMyThing对象一起使用
  • IEquatable<T>不是为此目的而设计的
  • 我不想玷污我的模型,只是为了使其可模拟

你会如何解决这个问题?


如果您正在尝试在HasThing()方法中测试一些复杂的场景,也许它应该接受IEnumerable<IMyThing>。为什么不“引入额外的间接层”呢? - shay__
4个回答

8
我最终在Github上向Moq项目做出了贡献(参见问题#248)。通过这些更改,即使是接口模拟,也可以模拟object.Equals(object obj)object.GetHashCode()object.ToString()
让我们看看它是否被接受。

1
我认为问题在于您模拟的接口中没有Equals方法。如果您创建一个具有可重写Equals方法的类(即使它什么也不做),则可以对其进行模拟。
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)));
}

请注意,如果您在MyTestThing上注释掉Equals方法,此测试将失败,因为Moq无法再模拟它。
如果您要创建这样的测试类,最好实现Equals方法,以便不必使用Moq进行设置。您甚至可以进一步创建一个抽象基类,其中唯一实现的方法是Equals,并在Moq测试中使用它,并从中派生出真正的实现。

这会起作用。一旦IMyThing在许多程序集中拥有众多成员和实现,情况就会变得尴尬。仅能为接口模拟行为将更加方便。 - koloman

1
如果您执行比较的方式会发生变化,那么您不应该使用直接比较,而是使用可以为您执行比较的对象。考虑像这样更改您的类:
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);
    }
}

然后你只需要模拟比较器。

这确实是一个有趣的解决方案!但是 HasThing(...) 的调用者并不一定知道 IMyThing 本身的实现。因此,它自然不知道要使用哪个相等比较器。因此,虽然这解决了我的测试问题,但它使实际代码比必要的更加复杂。最终,具体的 IMyThing 实现的比较根本不需要改变。你同意吗? - koloman

0
也许我没有完全理解你的意思,但是我没有看到 IMyThingEquals() 函数被扩展或重写? 所以我想我的第一种方法将是重写或扩展函数, 如果不起作用,我会使用 IMyThing : IEquatable<IMyThing>,我知道 IEquatable<T> 不是为此目的而设计的,但它很接近。 最后,我不认为你需要这样做,但它确实存在,只需创建一个类似于 bool IsEquals(IMyThing other){//check if equals} 的函数即可。

1
没有Equals()的具体覆盖,因为没有IMyThing的具体实现。我正在使用模拟框架Moq假装 有一个 IMyThing 的实现。因此,简单地覆盖Equals()是行不通的。实现一些IsEqual()方法也同样无效,原因相同。 - koloman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接