Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Arg<object>.Is.Equal with anonymous objects

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;
        }
    }
}
like image 622
MikeWyatt Avatar asked May 19 '11 14:05

MikeWyatt


People also ask

Can an object be anonymous?

An anonymous object is basically a value that has been created but has no name. Since they have no name, there's no other way to refer to them beyond the point where they are created. Consequently, they have “expression scope,” meaning they are created, evaluated, and destroyed everything within a single expression.

When we use anonymous object?

The anonymous object simply means creating a new Object without assigning it to a reference. Hence this could be used only once in the execution of a program as it is not referenced to any object. It is useful for writing implementation classes for listener interfaces in graphical programming.

How to make anonymous object in C#?

In C#, an anonymous type is a type (class) without any name that can contain public read-only properties only. It cannot contain other members, such as fields, methods, events, etc. You create an anonymous type using the new operator with an object initializer syntax.

Why is it impossible to use an anonymous type with a function parameter?

Anonymous type does not have an associated type identifier. So, it is impossible to use an anonymous type with a function parameter.


2 Answers

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...

like image 160
Jon Skeet Avatar answered Oct 12 '22 22:10

Jon Skeet


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;
        }
    }
}
like image 1
Simon Laing Avatar answered Oct 13 '22 00:10

Simon Laing