【问题标题】:Arg<object>.Is.Equal with anonymous objectsArg<object>.Is.Equal 与匿名对象
【发布时间】:2011-05-19 14:49:13
【问题描述】:

在我的 MVC3 项目中,我使用 IUrlProvider 接口来包装 UrlHelper 类。在我的一个控制器操作中,我有一个这样的调用:

string url = _urlProvider.Action("ValidateCode", new { code = "spam-and-eggs" });

我想在我的单元测试中存根这个方法调用,它在一个单独的项目中。测试设置如下所示:

IUrlProvider urlProvider = MockRepository.GenerateStub<IUrlProvider>();

urlProvider.Stub(u => u.Action(
    Arg<string>.Is.Equal("ValidateCode"),
    Arg<object>.Is.Equal(new { code = "spam-and-eggs" }) ))
    .Return("http://www.mysite.com/validate/spam-and-eggs");

很遗憾,Arg&lt;object&gt;.Is.Equal(new { code = "spam-and-eggs" }) 不起作用,因为new { code = "spam-and-eggs" } != new { code = "spam-and-eggs" } 当匿名类型在不同的程序集中声明时。

那么,我可以在 Rhino Mocks 中使用另一种语法来检查程序集之间匿名对象之间的匹配字段值吗?

或者我应该像这样用一个类替换匿名对象声明?

public class CodeArg
{
    public string code { get; set; }

    public override bool Equals(object obj)
    {
        if(obj == null || GetType() != obj.GetType())
        {
            return false;
        }

        return code == ((CodeArg)obj).code;

    }

    public override int GetHashCode()
    {
        return code.GetHashCode();
    }
}

string url = _urlProvider.Action("ValidateCode", 
    new CodeArg { code = "spam-and-eggs" });

IUrlProvider urlProvider = MockRepository.GenerateStub<IUrlProvider>();

urlProvider.Stub(u => u.Action(
    Arg<string>.Is.Equal("ValidateCode"),
    Arg<CodeArg>.Is.Equal(new CodeArg { code = "spam-and-eggs" }) ))
    .Return("http://www.mysite.com/validate/spam-and-eggs");

编辑

如果我的单元测试与我的控制器在同一个项目中,那么比较匿名对象就可以了。因为它们是在单独的程序集中声明的,所以它们不会相等,即使它们具有相同的字段名称和值。比较不同命名空间中的方法创建的匿名对象似乎不是问题。

解决方案

我使用自定义 AbstractConstraint 将 Arg&lt;object&gt;.Is.Equal() 替换为 Arg&lt;object&gt;.Matches()

IUrlProvider urlProvider = MockRepository.GenerateStub<IUrlProvider>();

urlProvider.Stub(u => u.Action(
    Arg<string>.Is.Equal("ValidateCode"),
    Arg<object>.Matches(new PropertiesMatchConstraint(new { code = "spam-and-eggs" })) ))
    .Return("http://www.mysite.com/validate/spam-and-eggs");

public class PropertiesMatchConstraint : AbstractConstraint
{
    private readonly object _equal;

    public PropertiesMatchConstraint(object obj)
    {
        _equal = obj;
    }

    public override bool Eval(object obj)
    {
        if (obj == null)
        {
            return (_equal == null);
        }
        var equalType = _equal.GetType();
        var objType = obj.GetType();
        foreach (var property in equalType.GetProperties())
        {
            var otherProperty = objType.GetProperty(property.Name);
            if (otherProperty == null || property.GetValue(_equal, null) != otherProperty.GetValue(obj, null))
            {
                return false;
            }
        }
        return true;
    }

    public override string Message
    {
        get
        {
            string str = _equal == null ? "null" : _equal.ToString();
            return "equal to " + str;
        }
    }
}

【问题讨论】:

标签: c# unit-testing c#-4.0 rhino-mocks


【解决方案1】:

匿名类型以非常正常的方式实现 Equals 和 GetHashCode,为它们的每个子成员调用 GetHashCode 和 Equals。

所以这应该通过:

Assert.AreEqual(new { code = "spam-and-eggs" },
                new { code = "spam-and-eggs" });

换句话说,我怀疑你在错误的地方寻找问题。

请注意,您必须以完全正确的顺序指定属性 - 所以new { a = 0, b = 1 } 将不等于new { b = 1, a = 0 };这两个对象将属于不同的类型。

编辑:匿名类型实例创建表达式也必须在同一个程序集中。这无疑是本案的问题所在。

如果Equals 允许您指定IEqualityComparer&lt;T&gt;,您可能会构建一个能够通过从另一种类型的实例的属性创建一种类型的实例来比较具有相同属性的两种匿名类型,然后将其与相同类型的原件进行比较。当然,如果您使用嵌套的匿名类型,则需要递归地执行此操作,这可能会变得很难看...

【讨论】:

  • 经过进一步测试,我发现这仅适用于同一程序集中的匿名类型。由于我的单元测试与我的控制器位于不同的项目中,因此匿名类型将不相等。我会更新我的问题来说明这一点。
  • @MikeWyatt:提出了一种替代方法,可以让您继续使用匿名类型。
  • Equals 不接受 IEqualityComparer 参数,不幸的是。
  • @MikeWyatt:骗子。这很痛苦。
  • 我想通了! Arg&lt;T&gt;.Is.Equal(obj) 不支持任何类型的自定义比较,但 Arg&lt;T&gt;.Matches() 采用“AbstractConstraint”对象。我正在用我的实现来更新我的问题。
【解决方案2】:

由于 GetValue 返回一个装箱值,这似乎可以正常工作。

public class PropertiesMatchConstraint : AbstractConstraint
{
    private readonly object _equal;

    public PropertiesMatchConstraint(object obj)
    {
        _equal = obj;
    }

    public override bool Eval(object obj)
    {
        if (obj == null)
        {
            return (_equal == null);
        }
        var equalType = _equal.GetType();
        var objType = obj.GetType();
        foreach (var property in equalType.GetProperties())
        {
            var otherProperty = objType.GetProperty(property.Name);

            if (otherProperty == null || !_ValuesMatch(property.GetValue(_equal, null), otherProperty.GetValue(obj, null)))
            {
                return false;
            }
        }
        return true;
    }

    //fix for boxed conversions object:Guid != object:Guid when both values are the same guid - must call .Equals()
    private bool _ValuesMatch(object value, object otherValue)
    {
        if (value == otherValue)
            return true; //return early

        if (value != null)
            return value.Equals(otherValue);

        return otherValue.Equals(value);
    }

    public override string Message
    {
        get
        {
            string str = _equal == null ? "null" : _equal.ToString();
            return "equal to " + str;
        }
    }
}

【讨论】:

  • 此外,此约束仅使用 _equal 作为要在 obj 上定义的最少属性。不应该确保两个对象的属性相同吗?不多不少?
  • 谢谢!我开始使用问题中发布的代码并遇到了拳击问题。然而,我不同意你的评论。我希望平等匹配只出现在我感兴趣的字段上。
猜你喜欢
  • 1970-01-01
  • 2011-11-27
  • 1970-01-01
  • 2022-06-23
  • 1970-01-01
  • 2012-12-30
  • 1970-01-01
  • 2023-03-05
  • 1970-01-01
相关资源
最近更新 更多