【问题标题】:How to mock `object.Equals(object obj)` for an interface using Moq如何使用 Moq 模拟接口的`object.Equals(object obj)`
【发布时间】:2016-02-15 10:05:06
【问题描述】:

我有一个有趣的问题要解决。考虑一些像这样的界面:

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() 方法创建了一个拦截器,但它永远无法到达。而是调用对象代理的 equals 方法。我责怪我在模拟一个接口而不是一个具体的类。

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

当然,我可以像这样扩充IMyThing 界面:

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

LINQ 运算符将识别IEquatable&lt;T&gt; 接口并使用它。

我不想这样做,因为:

  • 这仅适用于其他 IMyThing 对象
  • IEquatable&lt;T&gt; 并非用于此目的
  • 我不想污染我的模型只是为了让它可模拟

你会怎么解决这个问题?

【问题讨论】:

  • 如果你在HasThing()方法中尝试测试一些复杂的场景,也许它应该接受IEnumerable&lt;IMyThing&gt;。为什么不“引入额外的间接层”?

标签: c# .net moq


【解决方案1】:

我最终为 Github 上的 Moq 项目贡献了代码(参见 issue #248)。通过这些更改,可以模拟 object.Equals(object obj)object.GetHashCode()object.ToString(),即使是接口模拟。

让我们看看它是否被接受。

【讨论】:

    【解决方案2】:

    我认为问题出在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 在众多程序集中拥有众多成员和众多实现,它就会变得尴尬。只要能够模拟接口的行为就会方便得多。
    【解决方案3】:

    如果您执行比较的方式可能发生变化,则不应使用直接比较,而应使用可以为您进行比较的对象。

    考虑像这样对您的班级进行更改:

    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 实现的比较根本没有变化。你同意吗?
    【解决方案4】:

    也许我不完全理解你,但我没有看到IMyThing 的扩展或覆盖Equals() 函数? 所以我想我的第一种方法将被覆盖或扩展功能, 如果它不起作用,我会IMyThing : IEquatable&lt;IMyThing&gt;,我知道IEquatable&lt;T&gt; 不是为此目的,但它关闭了。 最后一个,我认为你不需要这样做,但它存在,只需创建一个类似bool IsEquals(IMyThing other){//check if equals}

    的函数

    【讨论】:

    • Equals() 没有具体的覆盖,因为IMyThing 没有具体的实现。我正在使用模拟框架Moq假装如果IMyThing 有一个实现。因此,简单地覆盖 Equals() 是行不通的。出于同样的原因,实现一些IsEqual() 方法也不起作用。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-01-16
    • 2014-09-28
    • 1970-01-01
    • 2011-11-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多