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?
Community
  • 1
  • 1
citronas
  • 19,035
  • 27
  • 96
  • 164
  • 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 – Marshall Tigerus Nov 17 '14 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. – citronas Nov 17 '14 at 16:47
  • 1
    Does `SimplifiedClass` have any equality defined? – Jon Skeet Nov 17 '14 at 16:48
  • I've overwritten Equals and GetHashCode in SimplifiedClass. Since you are asking, I think I've missed something? – citronas Nov 17 '14 at 16:50
  • If you don't care about order why you are using `SequenceEqual` and not something like `Enumerable.Except`? – Alexei Levenkov Nov 17 '14 at 16:53
  • @AlexeiLevenkov: I've dismissed the idea of using multiple calls to Except, because it performs set operations, but I'm using a list which can contain duplicates. Think of this example: A={1,2,2}, B={1,1,2}. A.Except(B) and B.Except(A) are both empty. Although A.Count() == B.Count(), both lists are not the same, because they don't contain all numbers in the same amount. I've seen solutions that "group" by the item and count in a dictionary, but I'd rather see a more ".Net / Interface solution" – citronas Nov 17 '14 at 17:05
  • You should ask new question about it and clean this one so it ask only one problem (which you already have answer to here). Make sure to link to existing "GroupBy" solutions in your new question. – Alexei Levenkov Nov 17 '14 at 17:09
  • @AlexeiLevenkov: My last comment was only meant to be the answer to your question 'Why you are not using something like Enumerable.Except'. If I'm more interested about an elegant solution for the "GroupBy" solution in the future, I'll open a separate question. Right now, I'm more interested about object equality. – citronas Nov 17 '14 at 17:14
  • Hmmm... I'm lost now - so you are saying you've seen solutions to this with GroupBy+Dictionary, but you are looking for something else - what type of answer you are looking for than? Would you mind to clarify "more .Net / Interface solution" statement? (as without it current answer by Scott Chamberlain is not complete) – Alexei Levenkov Nov 17 '14 at 17:40
  • Important: `CollectionAssert.AreEqual` is not happy (fails) if the order of one collections is different from that of the other. Since you want to "basically ignore the order" you must use `CollectionAssert.AreEquivalent` instead. – Jeppe Stig Nielsen Nov 17 '14 at 20:06

1 Answers1

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.

Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431
  • 2
    +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). – Alexei Levenkov Nov 17 '14 at 16:57
  • 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. – citronas Nov 17 '14 at 16:58
  • @ScottChamberlain - indeed, it was not clear from original sample that collections can have duplicates. – Alexei Levenkov Nov 17 '14 at 17:32
  • 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 ;) – citronas Nov 18 '14 at 10:19