UnitTesting List<T> of custom objects with List<S> of custom objects for equality

1k views Asked by At

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?
1

There are 1 answers

4
Scott Chamberlain On BEST ANSWER

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.