Arg<object>.Is.Equal with anonymous objects

2019-06-22 00:15发布

问题:

In my MVC3 project, I use an IUrlProvider interface to wrap the UrlHelper class. In one of my controller actions, I have a call like this:

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

I want to stub this method call in my unit test, which is in a separate project. The test setup looks something like this:

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");

Unfortunately, Arg<object>.Is.Equal(new { code = "spam-and-eggs" }) doesn't work, because new { code = "spam-and-eggs" } != new { code = "spam-and-eggs" } when the anonymous types are declared in different assemblies.

So, is there an alternative syntax I can use with Rhino Mocks to check for matching field values between anonymous objects across assemblies?

Or should I replace the anonymous object declarations with a class, like this?

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");

EDIT

If my unit test was in the same project as my controller, comparing the anonymous objects would work fine. Because they are declared in separate assemblies, they will not be equal, even if they have the same field names and values. Comparing anonymous objects created by methods in different namespaces doesn't seem to be a problem.

SOLUTION

I replaced Arg<object>.Is.Equal() with Arg<object>.Matches() using a custom AbstractConstraint:

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

回答1:

Anonymous types do implement Equals and GetHashCode in a pretty normal way, calling GetHashCode and Equals for each of their submembers.

So this should pass:

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

In other words, I suspect you're looking for the problem in the wrong place.

Note that you have to specify the properties in exactly the right order - so new { a = 0, b = 1 } will not be equal to new { b = 1, a = 0 }; the two objects will be of different types.

EDIT: The anonymous type instance creation expressions have to be in the same assembly, too. This is no doubt the problem in this case.

If Equals allows you to specify an IEqualityComparer<T>, you could probably build one which is able to compare two anonymous types with the same properties by creating an instance of one type from the properties of an instance of the other, and then comparing that to the original of the same type. Of course if you were using nested anonymous types you'd need to do that recursively, which could get ugly...



回答2:

As GetValue returns a boxed value, this appears to work correctly.

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