How to Implement Unit Tests for Equals and GetHashCode Methods

by Zoran Horvat

Introduction

Implementing Equals method and its supporting methods, such as GetHashCode, is a relatively simple task. However, testing that all elements are in place is not always easy.

First of all, we should recall the purpose of the Equals method and why other methods should be implemented along with it. Equals accepts other object and tests whether current instance is equal to it in terms that suit current object. Two circles might be equal if they have the same radius. Alternatively, they might be equal if they have the same radius and the same center location. Which one of these tests is right depends on the application. For example, comparing just the radius is perfectly fine when circles are placed in the toolbox. But once they are moved to the canvas, we might want to enforce that both their location and size are checked for equality.

Another method which goes hand by hand with Equals is GetHashCode. This might not be so obvious, but these two methods are intended to be used together. And the glue is the hash table, which relies on GetHashCode to hash the items and then on Equals to resolve collisions. Hashing has been implemented in this way from the very beginning of .NET and, like it or not, we should override GetHashCode each time when we override the Equals method.

And that is not all. Overriding the Equals method effectively modifies comparison between objects of a type. Should operators == and != stay default, we might run a risk of having different outcomes when comparing using the equality operator and when using the Equals method. This implies that Equals override should go along with equality and inequality operator overrides.

Special care should be taken regarding nulls and types that are not the same as current object's type. In both cases, current object should be declared non-equal to the other object. With nulls this is quite obvious - none object is equal to null. With objects of other types it might not come that easy. Special case here is when object is compared against object of its derived type. The short question whether these two objects could and should be treated as equal is: No, they should be treated as different. The long answer can be found in How to Override Equals and GetHashCode Methods in Base and Derived Classes .

Final requirement, which is more a recommendation aimed to improve performance, is that the class should implement strongly typed IEquatable<T> interface. Compiler will then be able to link calls to Equals to strongly typed implementation, which lets us avoid dynamic typecasts at run time.

List of Requirements

The definite list of requirements when overriding Equals and GetHashCode methods and == and != operators is as follows:

  • Equals should return false when argument is null.
  • Equals should return false when argument is non-null value of a different type than the type of the current object.
  • Equals should return false when argument is non-null instance of the same type but semantically different from current object.
  • Equals should return true when argument is non-null instance of the same type and semantically equal to current object.
  • Strongly typed Equals should behave exactly the same as Equals method.
  • GetHashCode should return the same value when invoked on objects that are semantically equal. (Observe that there is no opposite requirement: to make results different when two objects are not equal; it would be impossible to enforce such a requirement.)
  • Equality operator returns true if both arguments are null.
  • Equality operator returns false if one argument is null and the other one is non-null.
  • Equality operator returns exactly the same result as Equals when both arguments are non-null.
  • Inequality operator returns false if both arguments are null.
  • Inequality operator returns true if one argument is null and the other one is non-null.
  • Inequality operator returns opposite result from Equals when both arguments are non-null.

Testing the Implementation

Preceding analysis leaves us with feeling that any class implementing Equals method can be tested in the same way. We just need to test whether it satisfies three groups of rules:

  • Equality with same objects,
  • Inequality with different objects, and
  • Inequality with null.

In this article we are providing a unified set of tests based on MSTest Framework, which are sufficient to test any implementation of Equals and surrounding methods. Tests are based on only three functions, each asserting one of the points stated above. We will first start with demonstration and then list the source code of the testing methods.

Demonstration

In this example we will manage cars which are organized by manufacturer and model. Below is the short console application which loads a collection of vehicle descriptions and then prints out the list. However, requirement says that vehicles with the same manufacturer and model should be grouped together before printing, and that is where Equals method will have its role. Here is the code:

// Vehicle.cs
namespace EqualityDemo
{
    public class Vehicle
    {

        private string manufacturer;
        private string model;
        private int productionYear;

        public Vehicle(string manufacturer, string model, int productionYear)
        {
            this.manufacturer = manufacturer;
            this.model = model;
            this.productionYear = productionYear;
        }

        public string GetLabel()
        {
            return string.Format("{0} {1}", this.manufacturer, this.model);
        }

    }
}

// Program.cs
using System;
using System.Collections.Generic;
using System.Linq;

namespace EqualityDemo
{

    public class Program
    {

        static void Main(string[] args)
        {

            IEnumerable<Vehicle> vehicles = LoadVehicles();

            ReportVehicles(vehicles);

            Console.ReadLine();

        }

        private static IEnumerable<Vehicle> LoadVehicles()
        {
            return new List<Vehicle>()
            {
                new Vehicle("Fiat", "Bravo",1995),
                new Vehicle("Opel", "Meriva",2004),
                new Vehicle("Renault", "Logan",2006),
                new Vehicle("Opel", "Speedster",2002),
                new Vehicle("Renault", "Clio",1996),
                new Vehicle("Fiat", "Bravo",1997),
                new Vehicle("Opel", "Meriva",2007),
                new Vehicle("Renault", "Fluence",2011),
                new Vehicle("Fiat", "Seicento",1998),
                new Vehicle("Renault", "Clio",2002)
            };
        }

        private static void ReportVehicles(IEnumerable<Vehicle> vehicles)
        {

            var query =
                (from v in vehicles
                 group v by v into g
                 select new { Vehicle = g.Key.GetLabel(), Count = g.Count() });

            foreach (var record in query)
                Console.WriteLine("{0,-17} x {1}", record.Vehicle, record.Count);
        }

    }
}

This application produces output which is clearly incorrect:

            
Fiat Bravo        x 1
Opel Meriva       x 1
Renault Logan     x 1
Opel Speedster    x 1
Renault Clio      x 1
Fiat Bravo        x 1
Opel Meriva       x 1
Renault Fluence   x 1
Fiat Seicento     x 1
Renault Clio      x 1
                
    

Each vehicle type is listed separately, despite the fact that some vehicles are of the same type. Core problem is that grouping operation did not go so well:

var query =
    (from v in vehicles
     group v by v into g
     select new { Vehicle = g.Key.GetLabel(), Count = g.Count() });

This piece of code doesn't work properly because Vehicle class does not implement custom Equals method. Without it, LINQ simply compares objects by reference and finds that all of them are distinct. Therefore, grouping fails.

Adding Tests for Equals and Other Methods

In this article we are applying tests-first methodology. This means that we are first going to install unit tests, and only then to implement the feature.

In order to make our application work we will define unit tests that prove that Equals, GetHashCode, equality and inequality operators are all implemented well. Here is the testing class which ensures that Vehicle class recognizes equality and inequality correctly.

// Vehicle_UnitTests.cs
using EqualityDemo.TestUtilities;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace EqualityDemo.UnitTests
{
    [TestClass]
    public class Vehicle_UnitTests
    {
        [TestMethod]
        public void Equality_SameManufacturerAndModelDistinctYear()
        {
            Vehicle vehicle1 = new Vehicle("Renault", "Fluence", 2011);
            Vehicle vehicle2 = new Vehicle("Renault", "Fluence", 2012);
            EqualityTests.TestEqualObjects(vehicle1, vehicle2);
        }

        [TestMethod]
        public void Inequality_SameManufacturerDistinctModel()
        {
            Vehicle vehicle1 = new Vehicle("Renault", "Fluence", 2011);
            Vehicle vehicle2 = new Vehicle("Renault", "Clio", 2011);
            EqualityTests.TestUnequalObjects(vehicle1, vehicle2);
        }

        [TestMethod]
        public void Inequality_DistinctManufacturerSameModel()
        {
            Vehicle vehicle1 = new Vehicle("Opel", "Fluence", 2011);
            Vehicle vehicle2 = new Vehicle("Renault", "Fluence", 2011);
            EqualityTests.TestUnequalObjects(vehicle1, vehicle2);
        }

        [TestMethod]
        public void Inequality_ComparingWithNull()
        {
            Vehicle vehicle = new Vehicle("Renault", "Fluence", 2011);
            EqualityTests.TestAgainstNull(vehicle);
        }
    }
}

Major quality of this test class is its simplicity. Observe how all probing logic is moved out from the test class. Tests only concern with actual situations: whether two Vehicle objects are equal depending on their manufacturer and model values.

When these tests are run, they all fail, and these are the failure messages:

            
Equality_SameManufacturerAndModelDistinctYear
Assert.IsTrue failed. Some tests have failed:
GetHashCode of equal objects returned different values.
Equals returns False on equal objects.
Type does not override equality operator.
Type does not override inequality operator.

Inequality_SameManufacturerDistinctModel
Assert.IsTrue failed. Some tests have failed:
Type does not override equality operator.
Type does not override inequality operator.

Inequality_DistinctManufacturerSameModel
Assert.IsTrue failed. Some tests have failed:
Type does not override equality operator.
Type does not override inequality operator.

Inequality_ComparingWithNull
Assert.IsTrue failed. Some tests have failed:
Type does not override equality operator.
Type does not override inequality operator.
                
    

This output clearly explains what's wrong with the Vehicle class. We can now just walk down the list and implement the missing methods:

// Vehicle.cs
namespace EqualityDemo
{
    public class Vehicle
    {

        private string manufacturer;
        private string model;
        private int productionYear;

        public Vehicle(string manufacturer, string model, int productionYear)
        {
            this.manufacturer = manufacturer;
            this.model = model;
            this.productionYear = productionYear;
        }

        public string GetLabel()
        {
            return string.Format("{0} {1}", this.manufacturer, this.model);
        }

        public override int GetHashCode()
        {
            return this.manufacturer.GetHashCode() ^ this.model.GetHashCode();
        }

        public override bool Equals(object obj)
        {

            Vehicle vehicle = obj as Vehicle;

            if (vehicle != null)
                return this.manufacturer.Equals(vehicle.manufacturer) &&
                       this.model.Equals(vehicle.model);

            return false;

        }

        public static bool operator ==(Vehicle vehicle, Vehicle other)
        {
            return vehicle.Equals(other);
        }

        public static bool operator !=(Vehicle vehicle, Vehicle other)
        {
            return !(vehicle == other);
        }

    }
}

If we re-run the tests, the output would look like this:

            
Equality_SameManufacturerAndModelDistinctYear
Assert.IsTrue failed. Some tests have failed:
Equals threw NullReferenceException: Object reference not set to an instance of an object.
Operator == threw TargetInvocationException: Exception has been thrown by the target of an invocation.
Operator != threw TargetInvocationException: Exception has been thrown by the target of an invocation.

Inequality_SameManufacturerDistinctModel
Assert.IsTrue failed. Some tests have failed:
Equals threw NullReferenceException: Object reference not set to an instance of an object.
Equals threw NullReferenceException: Object reference not set to an instance of an object.
Operator == threw TargetInvocationException: Exception has been thrown by the target of an invocation.
Operator != threw TargetInvocationException: Exception has been thrown by the target of an invocation.

Inequality_DistinctManufacturerSameModel
Assert.IsTrue failed. Some tests have failed:
Equals threw NullReferenceException: Object reference not set to an instance of an object.
Equals threw NullReferenceException: Object reference not set to an instance of an object.
Operator == threw TargetInvocationException: Exception has been thrown by the target of an invocation.
Operator != threw TargetInvocationException: Exception has been thrown by the target of an invocation.

Inequality_ComparingWithNull
Assert.IsTrue failed. Some tests have failed:
Equals threw NullReferenceException: Object reference not set to an instance of an object.
Operator == threw TargetInvocationException: Exception has been thrown by the target of an invocation.
Operator != threw TargetInvocationException: Exception has been thrown by the target of an invocation.
                
    

Well, it looks like we have made the things worse! Actually, implementations of the four functions listed in the Vehicle class are no good. First of all, Equals method tests whether object is null by using the inequality operator. But this operator indirectly relies on Equals to tell its result. We were lucky enough that some of the calls failed with NullReferenceException, or otherwise this operation would result in stack overflow. The only thing that we did right was GetHashCode method. Consequently, there is no trace of previous error regarding this method.

Now we can start refining the implementations:

// Vehicle.cs
namespace EqualityDemo
{
    public class Vehicle
    {

        private string manufacturer;
        private string model;
        private int productionYear;

        public Vehicle(string manufacturer, string model, int productionYear)
        {
            this.manufacturer = manufacturer;
            this.model = model;
            this.productionYear = productionYear;
        }

        public string GetLabel()
        {
            return string.Format("{0} {1}", this.manufacturer, this.model);
        }

        public override int GetHashCode()
        {
            return this.manufacturer.GetHashCode() ^ this.model.GetHashCode();
        }

        public override bool Equals(object obj)
        {

            Vehicle vehicle = obj as Vehicle;

            if (!object.ReferenceEquals(vehicle, null))
                return this.manufacturer.Equals(vehicle.manufacturer) &&
                       this.model.Equals(vehicle.model);

            return false;

        }

        public static bool operator ==(Vehicle vehicle, Vehicle other)
        {

            bool isVehicleNull = object.ReferenceEquals(vehicle, null);
            bool isOtherNull = object.ReferenceEquals(other, null);

            if (isVehicleNull && isOtherNull)
                return true;
            else if (isVehicleNull)
                return false;
            else
                return vehicle.Equals(other);

        }

        public static bool operator !=(Vehicle vehicle, Vehicle other)
        {
            return !(vehicle == other);
        }

    }
}

When these changes are made, all tests become green.

What remains last is to add implementation of IEquatable<T> interface:

// Vehicle.cs
using System;

namespace EqualityDemo
{
    public class Vehicle: IEquatable<Vehicle>
    {
        ...
        public bool Equals(Vehicle other)
        {
            throw new NotImplementedException();
        }
    }
}

EqualityTests class will now detect that the class implements IEquatable<T> interface and it will try to test its implementation. This will cause all four tests to fail with the same list of errors:

            
Assert.IsTrue failed. Some tests have failed:
Strongly typed Equals threw NotImplementedException: The method or operation is not implemented.
Operator == threw TargetInvocationException: Exception has been thrown by the target of an invocation.
Operator != threw TargetInvocationException: Exception has been thrown by the target of an invocation.
                
    

Now we can get on to work of implementing the interface properly. One change will be made to existing code - strongly typed Equals method will become the one that does all the work, while general Equals method will just relay the call to it. Here is the implementation which makes all tests go green again:

// Vehicle.cs
using System;

namespace EqualityDemo
{
    public class Vehicle: IEquatable<Vehicle>
    {
        ...
        public override bool Equals(object obj)
        {
            return this.Equals(obj as Vehicle);
        }

        public bool Equals(Vehicle other)
        {
            if (other != null)
                return this.manufacturer.Equals(other.manufacturer) &&
                       this.model.Equals(other.model);
            return false;
        }
    }
}

When this class is inserted into the console application, we can observe that application groups Vehicle objects correctly:

            
Fiat Bravo        x 2
Opel Meriva       x 2
Renault Logan     x 1
Opel Speedster    x 1
Renault Clio      x 2
Renault Fluence   x 1
Fiat Seicento     x 1
                
    

Implementation of EqualityTests Class

Below is the full listing of the EqualityTests utility class. This class exposes three public static methods that can be used to test equality features of any type.

// EqualityTests.cs
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace EqualityDemo.TestUtilities
{
    public static class EqualityTests
    {

        private struct TestResult
        {

            public bool IsSuccess { get; set; }
            public string ErrorMessage { get; set; }

            public static TestResult CreateSuccess()
            {
                return new TestResult()
                {
                    IsSuccess = true
                };
            }

            public static TestResult CreateFailure(string message)
            {
                return new TestResult()
                {
                    IsSuccess = false,
                    ErrorMessage = message
                };
            }

        }

        public static void TestEqualObjects<T>(T obj1, T obj2)
        {

            ThrowIfAnyIsNull(obj1, obj2);

            IList<TestResult> testResults = new List<TestResult>()
            {
                TestGetHashCodeOnEqualObjects<T>(obj1, obj2),
                TestEquals<T>(obj1, obj2, true),
                TestEqualsOfT<T>(obj1, obj2, true),
                TestEqualityOperator<T>(obj1, obj2, true),
                TestInequalityOperator<T>(obj1, obj2, false)
            };

            AssertAllTestsHavePassed(testResults);

        }

        public static void TestUnequalObjects<T>(T obj1, T obj2)
        {

            ThrowIfAnyIsNull(obj1, obj2);

            IList<TestResult> testResults = new List<TestResult>()
            {
                TestEqualsReceivingNonNullOfOtherType<T>(obj1),
                TestEquals<T>(obj1, obj2, false),
                TestEqualsOfT<T>(obj1, obj2, false),
                TestEqualityOperator<T>(obj1, obj2, false),
                TestInequalityOperator<T>(obj1, obj2, true)
            };

            AssertAllTestsHavePassed(testResults);

        }

        public static void TestAgainstNull<T>(T obj)
        {

            ThrowIfAnyIsNull(obj);

            IList<TestResult> testResults = new List<TestResult>()
            {
                TestEqualsReceivingNull<T>(obj),
                TestEqualsOfTReceivingNull<T>(obj),
                TestEqualityOperatorReceivingNull<T>(obj),
                TestInequalityOperatorReceivingNull<T>(obj),
            };

            AssertAllTestsHavePassed(testResults);

        }

        private static TestResult
            TestGetHashCodeOnEqualObjects<T>(T obj1, T obj2)
        {

            return SafeCall("GetHashCode", () =>
                {
                    if (obj1.GetHashCode() != obj2.GetHashCode())
                        return TestResult.CreateFailure(
                            "GetHashCode of equal objects " +
                            "returned different values.");
                    return TestResult.CreateSuccess();
                });

        }

        private static TestResult
            TestEqualsReceivingNonNullOfOtherType<T>(T obj)
        {
            return SafeCall("Equals", () =>
                {
                    if (obj.Equals(new object()))
                        return TestResult.CreateFailure(
                            "Equals returned true when comparing " +
                            "with object of a different type.");
                    return TestResult.CreateSuccess();
                });
        }

        private static TestResult TestEqualsReceivingNull<T>(T obj)
        {
            if (typeof(T).IsClass)
                return TestEquals<T>(obj, default(T), false);
            return TestResult.CreateSuccess();
        }

        private static TestResult
            TestEqualsOfTReceivingNull<T>(T obj)
        {
            if (typeof(T).IsClass)
                return TestEqualsOfT<T>(obj, default(T), false);
            return TestResult.CreateSuccess();
        }

        private static TestResult TestEquals<T>(T obj1, T obj2,
                                                bool expectedEqual)
        {
            return SafeCall("Equals", () =>
                {
                    if (obj1.Equals((object)obj2) != expectedEqual)
                    {
                        string message =
                            string.Format("Equals returns {0} " +
                                          "on {1}equal objects.",
                                          !expectedEqual,
                                          expectedEqual ? "" : "non-");
                        return TestResult.CreateFailure(message);
                    }
                    return TestResult.CreateSuccess();
                });
        }

        private static TestResult TestEqualsOfT<T>(T obj1, T obj2,
                                                   bool expectedEqual)
        {
            if (obj1 is IEquatable<T>)
                return TestEqualsOfTOnIEquatable<T>(obj1 as IEquatable<T>,
                                                    obj2, expectedEqual);
            return TestResult.CreateSuccess();
        }

        private static TestResult
            TestEqualsOfTOnIEquatable<T>(IEquatable<T> obj1, T obj2,
                                         bool expectedEqual)
        {
            return SafeCall("Strongly typed Equals", () =>
                {
                    if (obj1.Equals(obj2) != expectedEqual)
                    {
                        string message =
                            string.Format("Strongly typed Equals " +
                                          "returns {0} on {1}equal " +
                                          "objects.",
                                          !expectedEqual,
                                          expectedEqual ? "" : "non-");
                        return TestResult.CreateFailure(message);
                    }
                    return TestResult.CreateSuccess();
                });
        }

        private static TestResult
            TestEqualityOperatorReceivingNull<T>(T obj)
        {
            if (typeof(T).IsClass)
                return TestEqualityOperator<T>(obj, default(T), false);
            return TestResult.CreateSuccess();
        }

        private static TestResult
            TestEqualityOperator<T>(T obj1, T obj2, bool expectedEqual)
        {
            MethodInfo equalityOperator = GetEqualityOperator<T>();
            if (equalityOperator == null)
                return TestResult.CreateFailure("Type does not override " +
                                                "equality operator.");
            return TestEqualityOperator<T>(obj1, obj2, expectedEqual,
                                           equalityOperator);
        }

        private static TestResult
            TestEqualityOperator<T>(T obj1, T obj2, bool expectedEqual,
                                    MethodInfo equalityOperator)
        {
            return SafeCall("Operator ==", () =>
                {
                    bool equal =
                        (bool)equalityOperator.Invoke(null,
                                                      new object[]
                                                      { obj1, obj2 });
                    if (equal != expectedEqual)
                    {
                        string message =
                            string.Format("Equality operator returned " +
                                          "{0} on {1}equal objects.",
                                          equal,
                                          expectedEqual ? "" : "non-");
                        return TestResult.CreateFailure(message);
                    }
                    return TestResult.CreateSuccess();
                });
        }

        private static TestResult
            TestInequalityOperatorReceivingNull<T>(T obj)
        {
            if (typeof(T).IsClass)
                return TestInequalityOperator<T>(obj, default(T), true);
            return TestResult.CreateSuccess();
        }

        private static TestResult
            TestInequalityOperator<T>(T obj1, T obj2,
                                      bool expectedUnequal)
        {
            MethodInfo inequalityOperator = GetInequalityOperator<T>();
            if (inequalityOperator == null)
                return TestResult.CreateFailure("Type does not override " +
                                                "inequality operator.");
            return TestInequalityOperator<T>(obj1, obj2, expectedUnequal,
                                             inequalityOperator);
        }

        private static TestResult
            TestInequalityOperator<T>(T obj1, T obj2,
                                      bool expectedUnequal,
                                      MethodInfo inequalityOperator)
        {
            return SafeCall("Operator !=", () =>
                {
                    bool unequal =
                        (bool)inequalityOperator.Invoke(null,
                                                        new object[]
                                                        { obj1, obj2 });
                    if (unequal != expectedUnequal)
                    {
                        string message =
                            string.Format("Inequality operator retrned " +
                                          "{0} when comparing {1}equal " +
                                          "objects.",
                                          unequal,
                                          expectedUnequal ? "non-" : "");
                        return TestResult.CreateFailure(message);
                    }
                    return TestResult.CreateSuccess();
                });
        }

        private static void ThrowIfAnyIsNull(params object[] objects)
        {
            if (objects.Any(o => object.ReferenceEquals(o, null)))
                throw new System.ArgumentNullException();
        }

        private static TestResult SafeCall(string functionName,
                                           Func<TestResult> test)
        {

            try
            {
                return test();
            }
            catch (System.Exception ex)
            {

                string message =
                    string.Format("{0} threw {1}: {2}",
                                  functionName,
                                  ex.GetType().Name,
                                  ex.Message);

                return TestResult.CreateFailure(message);

            }

        }

        private static MethodInfo GetEqualityOperator<T>()
        {
            return GetOperator<T>("op_Equality");
        }

        private static MethodInfo GetInequalityOperator<T>()
        {
            return GetOperator<T>("op_Inequality");
        }

        private static MethodInfo GetOperator<T>(string methodName)
        {
            BindingFlags bindingFlags =
                BindingFlags.Static |
                BindingFlags.Public;
            MethodInfo equalityOperator =
                typeof(T).GetMethod(methodName, bindingFlags);
            return equalityOperator;
        }

        private static void
            AssertAllTestsHavePassed(IList<TestResult> testResults)
        {

            bool allTestsPass =
                testResults
                .All(r => r.IsSuccess);
            string[] errors =
                testResults
                .Where(r => !r.IsSuccess)
                .Select(r => r.ErrorMessage)
                .ToArray();
            string compoundMessage =
                string.Join(Environment.NewLine, errors);

            Assert.IsTrue(allTestsPass,
                          "Some tests have failed:\n" +
                          compoundMessage);

        }

    }
}

Conclusion

In this article we have demonstrated how testing of equality-related features of a class can be completely offloaded to a utility class. This class can be applied to all types that require equality testing.

Some purists might object that EqualityTests class performs more than one test in each of the public methods. This would be a violation of the principle saying that one unit test should test one requirement for one method. However, we believe that stepping away from this principle pays itself quickly in terms of reduced custom code. In example above, Vehicle class has been fully covered with only four tests. If we decided to break these so that each unit test method tests only one requirement for one method, we would end up with 19 different test methods. That is why we believe that this design of EqualityTests class is well suited for practical use.


If you wish to learn more, please watch my latest video courses

About

Zoran Horvat

Zoran Horvat is the Principal Consultant at Coding Helmet, speaker and author of 100+ articles, and independent trainer on .NET technology stack. He can often be found speaking at conferences and user groups, promoting object-oriented and functional development style and clean coding practices and techniques that improve longevity of complex business applications.

  1. Pluralsight
  2. Udemy
  3. Twitter
  4. YouTube
  5. LinkedIn
  6. GitHub