Deep Equality in C# for Unit Testing

Adrian Jarzyna

Introduction

This article presents a short overview of methods of deep comparison between objects in Unit Tests. It is fact that C# and NUnit framework do not have any built-in deep equality of two complex objects. There is a few solutions for that issue, which will be presented below.

For every example of comparison method in this article will be used Person class and the same instances of the objects. 

 public static Person JohnSmith = new Person
        {
            Name = "John",
            Surname = "Smith",
            Gender = Gender.Male,
            Birthday = new DateTime(1972, 2, 2),
            Address = Address,
            Spouse = SussaneSmith,
            Children = new[] { MarrySmith, KewinSmith }
        };

        public static Person SussaneSmith = new Person
        {
            Name = "Sussane",
            Surname = "Smith",
            Gender = Gender.Female,
            Birthday = new DateTime(1976, 6, 6),
            Address = Address,
            Spouse = JohnSmith,
            Children = new[] { MarrySmith, KewinSmith } 
        };

        public static Person KewinSmith = new Person
        {
            Name = "Kewin",
            Surname = "Smith",
            Gender = Gender.Male,
            Birthday = new DateTime(2001, 1, 1),
            Address = Address
        };

        public static Person MarrySmith = new Person
        {
            Name = "Mary",
            Surname = "Smith",
            Gender = Gender.Female,
            Birthday = new DateTime(2002, 2, 2),
            Address = Address
        };

        public static readonly Address Address = new Address
        {
            Country = "London",
            City = "London",
            ZIPCode = "W1T 1JY",
            Street = "Tottenham Court Road",
            StreetNumber = 14,
            LocalNumber = 1
        };

Implementation of Equals in every class

Quoting Microsoft Developer Network default Equals(Object) method works this way:

  • „If the current instance is a reference type, the Equals(Object) method tests for reference equality, and a call to the Equals(Object) method is equivalent to a call to the ReferenceEquals method. Reference equality means that the object variables that are compared refer to the same object.”
  • “If the current instance is a value type, the Equals(Object) method tests for value equality.”

Default Equals(Object) method does only shallow comparison, does not compare subobjects of the class by values.

Because of that fact the first solution comes down to overwrite and implement Equals method in every class, which contains object.

Code example:

 [Serializable]
    public class Person
    {
 // … Properties code

        public override bool Equals(object obj)
        {
            if (obj == null) return false;
            if (obj == this) return true;
            var person = obj as Person;
            return person != null && Equals(person);
        }

        protected bool Equals(Person person)
        {
            return Name.Equals(person.Name)
                   && MiddleName.Equals(person.MiddleName)
                   && Surname.Equals(person.Surname)
                   && Gender.Equals(person.Gender)
                   && Birthday.Equals(person.Birthday)
                   && Address.Equals(person.Address)
                   && Spouse.Equals(person.Spouse)
                   && Children.SequenceEqual(person.Children);
        }
    }

    [Serializable]
    public class Address
    {
 // … Properties code

        public override bool Equals(object obj)
        {
            if (obj == null) return false;
            if (obj == this) return true;
            var address = obj as Address;
            return address != null && Equals(address);
        }

        protected bool Equals(Address address)
        {
            return Country.Equals(address.Country)
                   && ZIPCode.Equals(address.ZIPCode)
                   && City.Equals(address.City)
                   && Street.Equals(address.Street)
                   && StreetNumber.Equals(address.StreetNumber)
                   && LocalNumber.Equals(address.LocalNumber);
        }
    }

Failing test example:

 [Test]
        public void FailingExampleTest()
        {
            var obj1 = Data.JohnSmith;
            var obj2 = Data.SussaneSmith;

            Assert.AreEqual(obj1, obj2);
        }

Output of failing test:

Expected: <DeepEqualityProject.Class.Person>
But was:  <DeepEqualityProject.Class.Person>
Advantages:
  • The fastest and most efficient method of comparison
Disadvantages:
  • Every class should have implemented Equals method, optional GetHashCode method.
  • Lack of information about which property is not equal.

Xml serialization

The second solution is based on serialization of an object to XML format and comparing two strings.

Code example:

public class DeepEqualityUsingXmlSerializer
    {
        public static void ShouldDeepEqual<T>(T expected, T actual)
        {
            Assert.IsInstanceOf(expected.GetType(), actual);
            var x = Serialize(expected);
            var y = Serialize(actual);
            Assert.AreEqual(x, y);
        }

        public static string Serialize<T>(T obj)
        {
            if (obj == null)
            {
                return string.Empty;
            }

            try
            {
                var stringWriter = new StringWriter();
                var xmlWriter = XmlWriter.Create(stringWriter);
                var xmlSerializer = new XmlSerializer(typeof(T));
                xmlSerializer.Serialize(xmlWriter, obj);
                var serializeXml = stringWriter.ToString();
                xmlWriter.Close();
                return serializeXml;
            }
            catch (Exception e)
            {
                return string.Empty;
            }
        }
    }

Failing test example:

  [Test]
        public void FailingExampleTest()
        {
            var obj1 = Data.JohnSmith;
            var obj2 = Data.SussaneSmith;

            DeepEqualityUsingXmlSerializer.ShouldDeepEqual(obj1, obj2);
        }

 Output of failing test:

Expected string length 2027 but was 323. Strings differ at index 40.
  Expected: "...ion="1.0" encoding="utf-16"?><Person xmlns:xsi="http://www..."
  But was:  "...ion="1.0" encoding="utf-16"?><Address xmlns:xsi="http://ww..."
  --------------------------------------------^
Advantages:
  • It is quite simple to implement.
Disadvantages:
  • It is order sensitive what can cause misleading information about reason of a failing test, such as in example: Person object is comparing to Address object, because two object has different property enumeration order.
  • If we compare two xml representation of the object that were created by two different xlm serializers, we must ensure stable string representation of the objects that are equal (like 1.0 == 1 or name == NAME).

Json serialization

The same as XML serialization which is based on serialization of an object to JSON format and comparing two strings.

Code example:

 public class DeepEqualityUsingJsonSerialization 
    {
        public static void ShouldDeepEqual<T>(T expected, T actual)
        {
            Assert.IsInstanceOf(expected.GetType(), actual);
            var serializedExpected = Serialize(expected);
            var serializedActual = Serialize(actual);
            Assert.AreEqual(serializedExpected, serializedActual);
        }

        public static string Serialize<T>(T obj)
        {
            if (obj == null)
            {
                return string.Empty;
            }

            try
            {
                var stream1 = new MemoryStream();
                var ser = new DataContractJsonSerializer(typeof(Person));
                ser.WriteObject(stream1, obj);
                stream1.Position = 0;
                var streamReader = new StreamReader(stream1);
                return streamReader.ReadToEnd();
            }
            catch (Exception e)
            {
                return string.Empty;
            }
        }
    }

Failing test example:

  [Test]
        public void FailingExampleTest()
        {
            var obj1 = Data.JohnSmith;
            var obj2 = Data.SussaneSmith;

            DeepEqualityUsingJsonSerialization.ShouldDeepEqual(obj1, obj2);
        }

Output of failing test:

 Expected string length 3030 but was 1519. Strings differ at index 289.
  Expected: "...hday>k__BackingField":"\\/Date(65833200000+0100)\\/","<Child..."
  But was:  "...hday>k__BackingField":"\\/Date(202860000000+0200)\\/","<Chil..."
  ---------------------------------------------^
Advantages:
  • The same as in XML serialization.
Disadvantages:
  • The same as in XML serialization.
  • Less strict than XML serialization.

Reflection

The next solution is rested on comparing all fields by means of reflection mechanism. In case the field is of primitive type it is done comparison by usage method ToString(). In case of the field is class it is used a recursion.

Code example:

public class DeepEqualityUsingReflection {
        public static void ShouldDeepEqual(
     object expected, 
     object actual, 
     string fieldPath = "Field ",
            BindingFlags bindingFlags = BindingFlags.NonPublic |
                                        BindingFlags.Public |
                                        BindingFlags.Instance |
                                        BindingFlags.DeclaredOnly)
        {
            if (expected == null && actual == null)
                return;

            Assert.IsNotNull(expected);
            Assert.IsNotNull(actual);

            var type = expected.GetType();
            Assert.IsInstanceOf(type, actual);

            foreach (var field in type.GetFields(bindingFlags))
            {
                var fieldType = field.FieldType;
                var expectedValue = field.GetValue(expected);
                var acctualValue = field.GetValue(actual);

                if (fieldType.IsValueType)
                {
                    var expectedStringValue = expectedValue.ToString();
                    var actualStringlValue = acctualValue.ToString();
                    Assert.AreEqual(expectedStringValue, actualStringlValue, 
 fieldPath  + field.Name);
                }
                if (fieldType.IsClass)
                    ShouldDeepEqual(expectedValue, acctualValue, 
 fieldPath + field.Name + ".");
            }
        }
    }

Failing test example:

 [Test]
        public void FailingExampleTest()
        {
            var obj1 = Data.JohnSmith;
            var obj2 = Data.SussaneSmith;

            DeepEqualityUsingReflection.ShouldDeepEqual(obj1, obj2);
        }

Output of failing test:

Field <Name>k__BackingField.m_stringLength
  String lengths are both 1. Strings differ at index 0.
  Expected: "4"
  But was:  "7"
  -----------^
Advantages:
  • It is possible to check private fields.
  • In opposite to comparing by serialization to JSON or XML, comparing is done field by field.
  • It is opportunity to create own failing messages
Disadvantages:
  • The reflection mechanism is not as fast as it is in other solutions.

The 3rd party library

There is a few of 3rd party libraries that compare equality. In this example is used:

GITHUB

Failing test example:

 [Test]
        public void FailingExampleTest()
        {
            var obj1 = Data.JohnSmith;
            var obj2 = Data.SussaneSmith;

            obj1.ShouldDeepEqual(obj2);
        }

Output of failing test:

DeepEqual.Syntax.DeepEqualException : Comparison Failed: The following 4 differences were found.
   Actual.Name != Expected.Name ("John" != "Sussane")
   Actual.Gender != Expected.Gender (Male != Female)
   Actual.Birthday != Expected.Birthday (2/2/1972 12:00:00 AM != 6/6/1976 12:00:00 AM)  
   Actual.Spouse != Expected.Spouse (DeepEqualityProject.Class.Person != (null))
Advantages:
  • Some properties of the object can be excluded from the comparison.
  • The object is compared field by field.
  • The fail test message is more readable than serialization.

Summarize

To summarize, each of solution found a use in a proper case. Nevertheless, using the 3rd party libraries is the optimal solution.

Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑People!

Skontaktuj się z nami