Assert.AreEqual fallisce confrontando due oggetti che hanno identici valori per tutte le proprietà

Quando si confrontano due oggetti utilizzando Assert.AreEqual, il metodo fallisce se il confronto avviene tra due oggetti distinti, anche se questi hanno gli stessi valori per tutte le proprietà. Ad esempio, consideriamo il codice seguente:

  public class Prova
  {
    public Prova()
    {
    }

    public int Field1 { get; set; }
    public string Field2 { get; set; }
  }

  [TestClass]
  public class UnitTest1
  {
    [TestMethod]
    public void TestMethod1()
    {
      var a = new Prova() { Field1 = 1, Field2 = "a" };
      var b = new Prova() { Field1 = 1, Field2 = "a" };
      Assert.AreEqual(a, b);
    }
  }

Eseguendo il metodo di test UnitTest1, Asset.AreEqual fallisce. Il motivo è che il metodo utilizza l’uguaglianza tra reference, per cui i due oggetti non sono uguali se hanno gli stessi valori per tutte le proprietà, ma solo se fanno riferimento alla stessa istanza (stesso puntatore). In caso contrario sono due oggetti diversi, a prescindere dai valori delle singole proprietà.

Come fare allora a confrontare due istanze di una classe custom, in cui ad esempio il valore atteso è una istanza creata apposta per confrontarla con un valore che arriva da una query?

Ci possono essere diverse soluzioni.

La più semplice è quella di confrontare singolarmente le proprietà:

  [TestClass]
  public class UnitTest2
  {
    [TestMethod]
    public void TestMethod2()
    {
      var a = new Prova() { Field1 = 1, Field2 = "a" };
      var b = new Prova() { Field1 = 1, Field2 = "a" };
      Assert.AreEqual(a.Field1, b.Field1);
      Assert.AreEqual(a.Field2, b.Field2);
    }
  }

che poi è analoga, ma meno invasiva, di fare l’override di Equals (e ovviamente di GetHashCode) nella classe Prova, il che farebbe riuscire il TestMethod1 precedente:

    public override bool Equals(object obj)
    {
      if (!(obj is Prova)) return false;
      var objProva = (Prova)obj;
      return objProva.Field1 == this.Field1 && objProva.Field2 == this.Field2;
    }

    public override int GetHashCode()
    {
      return 11 * this.Field1.GetHashCode() + 13 * this.Field2.GetHashCode();
    }

In entrambi i casi, però, se si decidesse di aggiungere una nuova proprietà alla classe Prova, si dovrebbero riscrivere codice, o per i test o per i metodi Equals e GetHashCode.

Un’altra possibilità è quella illustrata in questo articolo, da cui ho ricopiato il codice di esempio. Con un po' di reflection, si scrive un metodo che fa al caso nostro e possiamo riutilizzare nei metodi di test (almeno per classi con proprietà di tipo valore):

   [TestMethod]
    public void TestMethod3()
    {
      var a = new Prova() { Field1 = 1, Field2 = "a" };
      var b = new Prova() { Field1 = 1, Field2 = "a" };
      LookLikeEachOther(a, b);
    }

    public static void LookLikeEachOther(object a, object b)
    {
      var typeA = a.GetType();
      var typeB = b.GetType();

      Assert.AreEqual(typeA, typeB, "The types of instances a and b are not the same.");

      var myProperties = typeA.GetProperties(BindingFlags.DeclaredOnly
                          | BindingFlags.Public | BindingFlags.Instance);

      foreach (var myPropertyA in myProperties)
      {
        var myPropertyB = typeB.GetProperty(myPropertyA.Name);
        Assert.IsNotNull(myPropertyB, string.Format(@"The property {0} from instance a 
           was not found on instance b.", myPropertyA.Name));

        Assert.AreEqual<Type>(myPropertyA.PropertyType, myPropertyB.PropertyType,
               string.Format(@"The type of property {0} on instance a is different from 
           the one on instance b.", myPropertyA.Name));

        Assert.AreEqual(myPropertyA.GetValue(a, null), myPropertyB.GetValue(b, null),
               string.Format(@"The value of the property {0} on instance a is different from 
           the value on instance b.", myPropertyA.Name));
      }
    }