Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Assert.AreEqual does not use my .Equals overrides on an IEnumerable implementation

I have a PagedModel class which implements IEnumerable to just return the ModelData, ignoring the paging data. I have also overridden Equals and GetHashCode to allow comparing two PagedModel objects by their ModelData, PageNumber, and TotalPages, and PageSize.

Here's the problem

Dim p1 As New PagedModel() With {
    .PageNumber = 1,
    .PageSize = 10,
    .TotalPages = 10,
    .ModelData = GetModelData()
}

Dim p2 As New PagedModel() With {
    .PageNumber = 1,
    .PageSize = 10,
    .TotalPages = 10,
    .ModelData = GetModelData()
}

p1.Equals(p2) =====> True
Assert.AreEqual(p1, p2) ======> False!

It looks like NUnit is calling it's internal EnumerableEqual method to compare my PagedModel's instead of using the Equals methods I provided! Is there any way to override this behavior, or do I have to write a custom Assertion.

like image 652
just.another.programmer Avatar asked May 24 '12 12:05

just.another.programmer


2 Answers

Doing what you are asking: I would advise against it but if you really don't like NUnit's behaviour and want to customize the assertion you can provide your own EqualityComparer.

Assert.That(p1, Is.EqualTo(p2).Using(myCustomEqualityComparer));

What you should be doing (short answer): You need GetHashCode and equals on ModelData instead of PagedModel since you are using PagedModel as the collection and ModelData as the elements.

What you should be doing (Long answer): Instead of overriding Equals(object) on PagedModel you need to implement IEquatable<T> on ModelData, where T is the type parameter to the IEnumerable, as well as override GetHashCode(). These two methods are what all IEnumerable methods in .Net use to determine equality (for operations such as Union, Distinct etc) when using the Default Equality Comparer (you don't specify your own IEqualityComparer).

The [Default Equality Comparer] checks whether type T implements the System.IEquatable interface and, if so, returns an EqualityComparer that uses that implementation. Otherwise, it returns an EqualityComparer that uses the overrides of Object.Equals and Object.GetHashCode provided by T.


To function correctly, GetHashCode needs to return the same results for all objects that return true for .Equals(T). The reverse is not necessarily true - GetHashCode can return collisions for objects that are not equal. More information here - see Marc Gravel's accepted answer. I have also found the implementation of GetHashCode in that answer using primes very useful.

like image 117
csauve Avatar answered Nov 18 '22 06:11

csauve


If you take a look at the implementation of the NUnit equality comparer in the GIT repo, you will see that there is a dedicated comparison block for two enumerations, which has a higher priority (simply because it is placed higher) than the comparisons using the IEquatable<T> interface or the Object.Equals(Object) method, which you have implemented or overloaded in your PagedModel class.

I don't know if this is a bug or a feature, but you probably should ask yourself first, if implementing the IEnumerable<ModelData> interface directly by your PagedModel class is actually the best option, especially because your PagedModel is something more than just an enumeration of ModelData instances.

Probably it would be enough (or even better) to provide the ModelData enumeration via a simple read-only IEnumerable<ModelData> property of the PagedModelclass. NUnit would stop looking at your PagedModel object as at a simple enumeration of ModelData objects and your unit tests would behave as expected.

The only other option is the one suggested by csauve; to implement a simple custom IComparer for your PagedModel and to supply an instance of it to all asserts where you will compare two PagedModel instances:

internal class PagedModelComparer : System.Collections.IComparer
{
    public static readonly IComparer Instance = new PagedModelComparer();

    private PagedModelComparer()
    {
    }

    public int Compare( object x, object y )
    {
        return x is PagedModel && ((PagedModel)x).Equals( y );
    }
}

    ...
    [Test]
    ...
        Assert.That( actual, Is.EqualTo( expected ).Using( PagedModelComparer.Instance ) );
    ...

But this will make your tests more complicated than necessary and you will always have to think to use your special comparer whenever you are writing additional tests for the PagedModel.

like image 1
swalex Avatar answered Nov 18 '22 05:11

swalex