1

I'm writing some UnitTests for a parser and I'm stuck at comparing two List<T> where T is a class of my own, that contains another List<S>.

My UnitTest compares two lists and fails. The code in the UnitTest looks like this:

CollectionAssert.AreEqual(list1, list2, "failed");

I've written a test scenario that should clarify my question:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ComparerTest
{
    class Program
    {
        static void Main(string[] args)
        {
            List<SimplifiedClass> persons = new List<SimplifiedClass>()
            {
                new SimplifiedClass()
                {
                 FooBar = "Foo1",
                  Persons = new List<Person>()
                  {
                    new Person(){ ValueA = "Hello", ValueB="Hello"},
                    new Person(){ ValueA = "Hello2", ValueB="Hello2"},
                  }
                }
            };
            List<SimplifiedClass> otherPersons = new List<SimplifiedClass>()
            {
                new SimplifiedClass()
                {
                 FooBar = "Foo1",
                  Persons = new List<Person>()
                  {
                    new Person(){ ValueA = "Hello2", ValueB="Hello2"},
                    new Person(){ ValueA = "Hello", ValueB="Hello"},
                  }
                }
            };
            // The goal is to ignore the order of both lists and their sub-lists.. just check if both lists contain the exact items (in the same amount). Basically ignore the order

            // This is how I try to compare in my UnitTest:
            //CollectionAssert.AreEqual(persons, otherPersons, "failed");
        }
    }

    public class SimplifiedClass
    {
        public String FooBar { get; set; }
        public List<Person> Persons { get; set; }

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

            PersonComparer personComparer = new PersonComparer();
            SimplifiedClass obj2 = (SimplifiedClass)obj;
            return this.FooBar == obj2.FooBar && Enumerable.SequenceEqual(this.Persons, obj2.Persons, personComparer); // I think here is my problem
        }

        public override int GetHashCode()
        {
            return this.FooBar.GetHashCode() * 117 + this.Persons.GetHashCode();
        }
    }

    public class Person
    {
        public String ValueA { get; set; }
        public String ValueB { get; set; }

        public override bool Equals(object obj)
        {
            if (obj == null)
            {
                return false;
            }
            Person obj2 = (Person)obj;
            return this.ValueA == obj2.ValueA && this.ValueB == obj2.ValueB;
        }

        public override int GetHashCode()
        {
            if (!String.IsNullOrEmpty(this.ValueA))
            {
                //return this.ValueA.GetHashCode() ^ this.ValueB.GetHashCode();
                return this.ValueA.GetHashCode() * 117 + this.ValueB.GetHashCode();
            }
            else
            {
                return this.ValueB.GetHashCode();
            }
        }

    }

    public class PersonComparer : IEqualityComparer<Person>
    {
        public bool Equals(Person x, Person y)
        {
            if (x != null)
            {
                return x.Equals(y);
            }
            else
            {
                return y == null;
            }
        }

        public int GetHashCode(Person obj)
        {
            return obj.GetHashCode();
        }
    }
}

The question is strongly related to C# Compare Lists with custom object but ignore order, but I can't find the difference, other than I wrap a list into another object and use the UnitTest one level above.

I've tried to use an IEqualityComparer:

public class PersonComparer : IEqualityComparer<Person>
{
    public bool Equals(Person x, Person y)
    {
        if (x != null)
        {
            return x.Equals(y);
        }
        else
        {
            return y == null;
        }
    }

    public int GetHashCode(Person obj)
    {
        return obj.GetHashCode();
    }
}

Afterwards I've tried to implement the ''IComparable'' interface thats allows the objects to be ordered. (Basically like this: https://stackoverflow.com/a/4188041/225808) However, I don't think my object can be brought into a natural order. Therefore I consider this a hack, if I come up with random ways to sort my class.

public class Person : IComparable<Person>
public int CompareTo(Person other)
{
  if (this.GetHashCode() > other.GetHashCode()) return -1;
  if (this.GetHashCode() == other.GetHashCode()) return 0;
  return 1;
}

I hope I've made no mistakes while simplifying my problem. I think the main problems are:

  1. How can I allow my custom objects to be comparable and define the equality in SimplifiedClass, that relies on the comparision of subclasses (e.g. Person in a list, like List<Person>). I assume Enumerable.SequenceEqual should be replaced with something else, but I don't know with what.
  2. Is CollectionAssert.AreEqual the correct method in my UnitTest?
10
  • 1
    what are you trying to validate. Are you concerned that the objects are identical? The order is identical? The count is identical? Have you tried CollectionAssert.AreEquivalent Commented Nov 17, 2014 at 16:45
  • The order of the objects is irrelevant, which I try to inplement. The count should also be identical. The order of the objects in any list is irrelevant. Commented Nov 17, 2014 at 16:47
  • 1
    Does SimplifiedClass have any equality defined? Commented Nov 17, 2014 at 16:48
  • I've overwritten Equals and GetHashCode in SimplifiedClass. Since you are asking, I think I've missed something? Commented Nov 17, 2014 at 16:50
  • If you don't care about order why you are using SequenceEqual and not something like Enumerable.Except? Commented Nov 17, 2014 at 16:53

1 Answer 1

5

Equals on a List<T> will only check reference equality between the lists themselves, it does not attempt to look at the items in the list. And as you said you don't want to use SequenceEqual because you don't care about the ordering. In that case you should use CollectionAssert.AreEquivalent, it acts just like Enumerable.SequenceEqual however it does not care about the order of the two collections.

For a more general method that can be used in code it will be a little more complicated, here is a re-implemented version of what Microsoft is doing in their assert method.

public static class Helpers
{
    public static bool IsEquivalent(this ICollection source, ICollection target)
    {
        //These 4 checks are just "shortcuts" so we may be able to return early with a result
        // without having to do all the work of comparing every member.
        if (source == null != (target == null))
            return false; //If one is null and one is not, return false immediately.
        if (object.ReferenceEquals((object)source, (object)target) || source == null)
            return true; //If both point to the same reference or both are null (We validated that both are true or both are false last if statement) return true;
        if (source.Count != target.Count)
            return false; //If the counts are different return false;
        if (source.Count == 0)
            return true; //If the count is 0 there is nothing to compare, return true. (We validated both counts are the same last if statement).

        int nullCount1;
        int nullCount2;

        //Count up the duplicates we see of each element.
        Dictionary<object, int> elementCounts1 = GetElementCounts(source, out nullCount1);
        Dictionary<object, int> elementCounts2 = GetElementCounts(target, out nullCount2);

        //It checks the total number of null items in the collection.
        if (nullCount2 != nullCount1)
        {
            //The count of nulls was different, return false.
            return false;
        }
        else
        {
            //Go through each key and check that the duplicate count is the same for 
            // both dictionaries.
            foreach (object key in elementCounts1.Keys)
            {
                int sourceCount;
                int targetCount;
                elementCounts1.TryGetValue(key, out sourceCount);
                elementCounts2.TryGetValue(key, out targetCount);
                if (sourceCount != targetCount)
                {
                    //Count of duplicates for a element where different, return false.
                    return false;
                }
            }

            //All elements matched, return true.
            return true;
        }
    }

    //Builds the dictionary out of the collection, this may be re-writeable to a ".GroupBy(" but I did not take the time to do it.
    private static Dictionary<object, int> GetElementCounts(ICollection collection, out int nullCount)
    {
        Dictionary<object, int> dictionary = new Dictionary<object, int>();
        nullCount = 0;
        foreach (object key in (IEnumerable)collection)
        {
            if (key == null)
            {
                ++nullCount;
            }
            else
            {
                int num;
                dictionary.TryGetValue(key, out num);
                ++num;
                dictionary[key] = num;
            }
        }
        return dictionary;
    }
}

What it does is it makes a dictionary out of the two collections, counting the duplicates and storing it as the value. It then compares the two dictionaries to make sure that the duplicate count matches for both sides. This lets you know that {1, 2, 2, 3} and {1, 2, 3, 3} are not equal where Enumerable.Execpt would tell you that they where.

Sign up to request clarification or add additional context in comments.

4 Comments

+1 - Note, based on code shown equivalence of 2 lists is actually part of real code, not the unit test. Make sure not to use CollectionAssert outside the test (Enumerable.Except may be the one to use).
Ok, this answers my second question, thanks. However, I can only use CollectionAssert.AreEquivalent in my UnitTest and not in SimplifiedClass.Equals. I'm still not sure, what I should use instead of Enumerable.SequenceEqual inside my SimplifiedClass.
@ScottChamberlain - indeed, it was not clear from original sample that collections can have duplicates.
Thanks. Too bad I didn't come up with the idea of looking up the implementations of the UnitTest methods myself ;) Initially I was hoping I would just have forgotten to implement a certain interface for this kind of comparision. It seems that there is no one or two line solution to my problem, but I'll stick with a reimplementation of CollectionAssert.AreEquivalent ;)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.