Arg <object> .Is.Equal with anonymous objects </object>

时间:2011-05-19 14:49:13

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

在我的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<object>.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<object>.Is.Equal()替换为Arg<object>.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;
        }
    }
}

2 个答案:

答案 0 :(得分: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<T>,则可以通过从实例的属性创建一个类型的实例来构建一个能够比较两个具有相同属性的匿名类型的类型另一个,然后将其与相同类型的原始数据进行比较。当然,如果你使用嵌套的匿名类型,你需要递归地执行,这可能会变得丑陋......

答案 1 :(得分:1)

当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;
        }
    }
}