web-dev-qa-db-fra.com

Comparer l'égalité entre deux objets dans NUnit

J'essaie d'affirmer qu'un objet est "égal" à un autre objet. 

Les objets ne sont que des instances d'une classe avec un tas de propriétés publiques. Existe-t-il un moyen simple de faire en sorte que NUnit affirme l’égalité en fonction des propriétés?

C'est ma solution actuelle mais je pense qu'il y a peut-être quelque chose de mieux:

Assert.AreEqual(LeftObject.Property1, RightObject.Property1)
Assert.AreEqual(LeftObject.Property2, RightObject.Property2)
Assert.AreEqual(LeftObject.Property3, RightObject.Property3)
...
Assert.AreEqual(LeftObject.PropertyN, RightObject.PropertyN)

Ce que je vais faire serait dans le même esprit que la CollectionEquivalentConstraint dans laquelle NUnit vérifie que le contenu de deux collections est identique.

112
Michael Haren

Remplacez les .Equals pour votre objet et dans le test unitaire, vous pouvez alors simplement faire ceci:

Assert.AreEqual(LeftObject, RightObject);

Bien sûr, cela pourrait signifier que vous déplacez simplement toutes les comparaisons individuelles vers la méthode .Equals, mais cela vous permettrait de réutiliser cette implémentation pour plusieurs tests, et il est probablement logique que les objets puissent se comparer à leurs frères et sœurs de toute façon.

Si vous ne pouvez pas remplacer Equals pour quelque raison que ce soit, vous pouvez créer une méthode d'assistance qui parcourt les propriétés publiques par réflexion et assigne chaque propriété. Quelque chose comme ça:

public static class AssertEx
{
    public static void PropertyValuesAreEquals(object actual, object expected)
    {
        PropertyInfo[] properties = expected.GetType().GetProperties();
        foreach (PropertyInfo property in properties)
        {
            object expectedValue = property.GetValue(expected, null);
            object actualValue = property.GetValue(actual, null);

            if (actualValue is IList)
                AssertListsAreEquals(property, (IList)actualValue, (IList)expectedValue);
            else if (!Equals(expectedValue, actualValue))
                Assert.Fail("Property {0}.{1} does not match. Expected: {2} but was: {3}", property.DeclaringType.Name, property.Name, expectedValue, actualValue);
        }
    }

    private static void AssertListsAreEquals(PropertyInfo property, IList actualList, IList expectedList)
    {
        if (actualList.Count != expectedList.Count)
            Assert.Fail("Property {0}.{1} does not match. Expected IList containing {2} elements but was IList containing {3} elements", property.PropertyType.Name, property.Name, expectedList.Count, actualList.Count);

        for (int i = 0; i < actualList.Count; i++)
            if (!Equals(actualList[i], expectedList[i]))
                Assert.Fail("Property {0}.{1} does not match. Expected IList with element {1} equals to {2} but was IList with element {1} equals to {3}", property.PropertyType.Name, property.Name, expectedList[i], actualList[i]);
    }
}
114
Juanma

Ne substituez pas Equals uniquement à des fins de test. C'est fastidieux et affecte la logique de domaine . Au lieu de cela,

Utiliser JSON pour comparer les données de l'objet

Aucune logique supplémentaire sur vos objets. Aucune tâche supplémentaire pour les tests. 

Il suffit d'utiliser cette méthode simple:

public static void AreEqualByJson(object expected, object actual)
{
    var serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
    var expectedJson = serializer.Serialize(expected);
    var actualJson = serializer.Serialize(actual);
    Assert.AreEqual(expectedJson, actualJson);
}

Cela semble aller très bien. Les informations sur les résultats de l'exécutant de test afficheront la comparaison de chaîne JSON (le graphe d'objet) incluse afin que vous puissiez voir directement ce qui ne va pas.

Notez aussi! Si vous avez des objets complexes plus gros et que vous voulez juste en comparer des parties, vous pouvez (utiliser LINQ pour les données de séquence) créer des objets anonymes à utiliser avec la méthode ci-dessus.

public void SomeTest()
{
    var expect = new { PropA = 12, PropB = 14 };
    var sut = loc.Resolve<SomeSvc>();
    var bigObjectResult = sut.Execute(); // This will return a big object with loads of properties 
    AssExt.AreEqualByJson(expect, new { bigObjectResult.PropA, bigObjectResult.PropB });
}
94
Max Wikstrom

Essayez la bibliothèque FluentAssertions:

dto.ShouldHave(). AllProperties().EqualTo(customer);

http://www.fluentassertions.com/

Il peut également être installé avec NuGet.

79
dkl

Je préfère ne pas remplacer Equals uniquement pour permettre les tests. N'oubliez pas que si vous remplacez Egal, vous devez également écraser GetHashCode, sinon vous risquez d'obtenir des résultats inattendus si vous utilisez vos objets dans un dictionnaire, par exemple.

J'aime l'approche de réflexion ci-dessus, car elle prévoit l'ajout de propriétés à l'avenir.

Cependant, pour une solution simple et rapide, il est souvent plus simple de créer une méthode d'assistance qui teste si les objets sont égaux, ou d'implémenter IEqualityComparer sur une classe que vous gardez confidentielle. Lorsque vous utilisez la solution IEqualityComparer, vous n'avez pas à vous soucier de la mise en oeuvre de GetHashCode. Par exemple:

// Sample class.  This would be in your main Assembly.
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// Unit tests
[TestFixture]
public class PersonTests
{
    private class PersonComparer : IEqualityComparer<Person>
    {
        public bool Equals(Person x, Person y)
        {
            if (x == null && y == null)
            {
                return true;
            }

            if (x == null || y == null)
            {
                return false;
            }

            return (x.Name == y.Name) && (x.Age == y.Age);
        }

        public int GetHashCode(Person obj)
        {
            throw new NotImplementedException();
        }
    }

    [Test]
    public void Test_PersonComparer()
    {
        Person p1 = new Person { Name = "Tom", Age = 20 }; // Control data

        Person p2 = new Person { Name = "Tom", Age = 20 }; // Same as control
        Person p3 = new Person { Name = "Tom", Age = 30 }; // Different age
        Person p4 = new Person { Name = "Bob", Age = 20 }; // Different name.

        Assert.IsTrue(new PersonComparer().Equals(p1, p2), "People have same values");
        Assert.IsFalse(new PersonComparer().Equals(p1, p3), "People have different ages.");
        Assert.IsFalse(new PersonComparer().Equals(p1, p4), "People have different names.");
    }
}
34
Chris Yoxall

J'ai essayé plusieurs approches mentionnées ici. La plupart impliquent la sérialisation de vos objets et la comparaison d'une chaîne. Bien que très facile et généralement très efficace, j’ai trouvé que c’était un peu court quand vous avez un échec et que quelque chose comme ça est signalé:

Expected string length 2326 but was 2342. Strings differ at index 1729.

Déterminer où sont les différences est une douleur pour le moins.

Avec les comparaisons de graphes objet de FluentAssertions (c'est-à-dire a.ShouldBeEquivalentTo(b)), vous obtenez ceci en retour:

Expected property Name to be "Foo" but found "Bar"

C'est beaucoup mieux. Get FluentAssertions maintenant, vous serez heureux plus tard (et si vous enregistrez cela par dessus, merci de passer également à la vote dkl où FluentAssertions a été suggéré pour la première fois).

13
Todd Menier

Je suis d'accord avec ChrisYoxall - implémenter Equals dans votre code principal uniquement à des fins de test n'est pas bon.

Si vous implémentez Equals parce que la logique de l'application l'exige, c'est très bien, mais conservez du code purement réservé aux tests, afin d'éviter toute encombrement (la sémantique de la vérification identique des tests peut être différente de celle requise par votre application).

En bref, gardez le code réservé aux tests en dehors de votre classe.

Une comparaison superficielle simple des propriétés utilisant la réflexion devrait suffire pour la plupart des classes, bien que vous ayez peut-être besoin de recurse si vos objets ont des propriétés complexes. Si vous suivez les références, méfiez-vous des références circulaires ou similaires.

Sournois

9
Sly Gryphon

Les contraintes de propriété , ajoutées à NUnit 2.4.2, permettent une solution plus lisible que celle d'origine du PO et produisent de bien meilleurs messages d'échec. Ce n'est en aucun cas générique, mais si vous n'avez pas besoin de le faire pour trop de classes, c'est une solution très adéquate.

Assert.That(ActualObject, Has.Property("Prop1").EqualTo(ExpectedObject.Prop1)
                          & Has.Property("Prop2").EqualTo(ExpectedObject.Prop2)
                          & Has.Property("Prop3").EqualTo(ExpectedObject.Prop3)
                          // ...

Pas aussi général que de mettre en œuvre Equals mais cela donne un bien meilleur message d'échec que

Assert.AreEqual(ExpectedObject, ActualObject);
4
Paul Hicks

La solution JSON de Max Wikstrom (ci-dessus) me semble la plus logique: elle est courte, propre et, surtout, elle fonctionne. Personnellement, je préférerais implémenter la conversion JSON en tant que méthode distincte et replacer l'assertion dans le test unitaire comme ceci ...

MÉTHODE D'AIDE:

public string GetObjectAsJson(object obj)
    {
        System.Web.Script.Serialization.JavaScriptSerializer oSerializer = new System.Web.Script.Serialization.JavaScriptSerializer();
        return oSerializer.Serialize(obj);
    }

TEST DE L'UNITÉ :

public void GetDimensionsFromImageTest()
        {
            Image Image = new Bitmap(10, 10);
            ImageHelpers_Accessor.ImageDimensions expected = new ImageHelpers_Accessor.ImageDimensions(10,10);

            ImageHelpers_Accessor.ImageDimensions actual;
            actual = ImageHelpers_Accessor.GetDimensionsFromImage(Image);

            /*USING IT HERE >>>*/
            Assert.AreEqual(GetObjectAsJson(expected), GetObjectAsJson(actual));
        }

FYI - Vous devrez peut-être ajouter une référence à System.Web.Extensions dans votre solution.

3
samaspin

C'est un très vieux fil de discussion mais je me demandais s'il n'y avait pas une raison pour laquelle aucune réponse proposée NUnit.Framework.Is.EqualTo et NUnit.Framework.Is.NotEqualTo?

Tel que:

Assert.That(LeftObject, Is.EqualTo(RightObject)); 

et

Assert.That(LeftObject, Is.Not.EqualTo(RightObject)); 
1
user2315856

Installez simplement ExpectedObjects de Nuget, vous pouvez facilement comparer la valeur de propriété de deux objets, chaque valeur d’objet de la collection, la valeur de deux objets composés et la valeur de propriété de comparaison partielle par type anonyme.

J'ai quelques exemples sur github: https://github.com/hatelove/CompareObjectEquals

Voici quelques exemples contenant des scénarios de comparaison d'objet:

    [TestMethod]
    public void Test_Person_Equals_with_ExpectedObjects()
    {
        //use extension method ToExpectedObject() from using ExpectedObjects namespace to project Person to ExpectedObject
        var expected = new Person
        {
            Id = 1,
            Name = "A",
            Age = 10,
        }.ToExpectedObject();

        var actual = new Person
        {
            Id = 1,
            Name = "A",
            Age = 10,
        };

        //use ShouldEqual to compare expected and actual instance, if they are not equal, it will throw a System.Exception and its message includes what properties were not match our expectation.
        expected.ShouldEqual(actual);
    }

    [TestMethod]
    public void Test_PersonCollection_Equals_with_ExpectedObjects()
    {
        //collection just invoke extension method: ToExpectedObject() to project Collection<Person> to ExpectedObject too
        var expected = new List<Person>
        {
            new Person { Id=1, Name="A",Age=10},
            new Person { Id=2, Name="B",Age=20},
            new Person { Id=3, Name="C",Age=30},
        }.ToExpectedObject();

        var actual = new List<Person>
        {
            new Person { Id=1, Name="A",Age=10},
            new Person { Id=2, Name="B",Age=20},
            new Person { Id=3, Name="C",Age=30},
        };

        expected.ShouldEqual(actual);
    }

    [TestMethod]
    public void Test_ComposedPerson_Equals_with_ExpectedObjects()
    {
        //ExpectedObject will compare each value of property recursively, so composed type also simply compare equals.
        var expected = new Person
        {
            Id = 1,
            Name = "A",
            Age = 10,
            Order = new Order { Id = 91, Price = 910 },
        }.ToExpectedObject();

        var actual = new Person
        {
            Id = 1,
            Name = "A",
            Age = 10,
            Order = new Order { Id = 91, Price = 910 },
        };

        expected.ShouldEqual(actual);
    }

    [TestMethod]
    public void Test_PartialCompare_Person_Equals_with_ExpectedObjects()
    {
        //when partial comparing, you need to use anonymous type too. Because only anonymous type can dynamic define only a few properties should be assign.
        var expected = new
        {
            Id = 1,
            Age = 10,
            Order = new { Id = 91 }, // composed type should be used anonymous type too, only compare properties. If you trace ExpectedObjects's source code, you will find it invoke config.IgnoreType() first.
        }.ToExpectedObject();

        var actual = new Person
        {
            Id = 1,
            Name = "B",
            Age = 10,
            Order = new Order { Id = 91, Price = 910 },
        };

        // partial comparing use ShouldMatch(), rather than ShouldEqual()
        expected.ShouldMatch(actual);
    }

Référence:

  1. ExpectedObjects github
  2. Introduction de ExpectedObjects
1
In91

https://github.com/kbilsted/StatePrinter a été écrit spécifiquement pour vider les graphiques d'objet dans une représentation sous forme de chaîne dans le but d'écrire des tests unitaires faciles. 

  • Il vient avec des méthodes Assert qui génèrent une chaîne correctement échappée copier-coller dans le test pour le corriger.
  • Il permet à unittest d'être automatiquement réécrit
  • Il s'intègre à tous les frameworks de tests unitaires
  • Contrairement à la sérialisation JSON, les références circulaires sont prises en charge.
  • Vous pouvez facilement filtrer, de sorte que seules des parties de types soient vidées

Donné

class A
{
  public DateTime X;
  public DateTime Y { get; set; }
  public string Name;
}

Vous pouvez taper de manière sécurisée et utiliser la complétion automatique de visual studio pour inclure ou exclure des champs. 

  var printer = new Stateprinter();
  printer.Configuration.Projectionharvester().Exclude<A>(x => x.X, x => x.Y);

  var sut = new A { X = DateTime.Now, Name = "Charly" };

  var expected = @"new A(){ Name = ""Charly""}";
  printer.Assert.PrintIsSame(expected, sut);
1
Carlo V. Dango

Une autre option consiste à écrire une contrainte personnalisée en implémentant la classe abstraite Constraint de NUnit. Avec une classe d'assistance fournissant un peu de sucre syntaxique, le code de test résultant est agréablement concis et lisible, par ex. 

Assert.That( LeftObject, PortfolioState.Matches( RightObject ) ); 

Pour un exemple extrême, considérons une classe qui a des membres 'en lecture seule', n'est pas IEquatable et vous ne pouvez pas changer la classe sous test même si vous vouliez:

public class Portfolio // Somewhat daft class for pedagogic purposes...
{
    // Cannot be instanitated externally, instead has two 'factory' methods
    private Portfolio(){ }

    // Immutable properties
    public string Property1 { get; private set; }
    public string Property2 { get; private set; }  // Cannot be accessed externally
    public string Property3 { get; private set; }  // Cannot be accessed externally

    // 'Factory' method 1
    public static Portfolio GetPortfolio(string p1, string p2, string p3)
    {
        return new Portfolio() 
        { 
            Property1 = p1, 
            Property2 = p2, 
            Property3 = p3 
        };
    }

    // 'Factory' method 2
    public static Portfolio GetDefault()
    {
        return new Portfolio() 
        { 
            Property1 = "{{NONE}}", 
            Property2 = "{{NONE}}", 
            Property3 = "{{NONE}}" 
        };
    }
}

Le contrat pour la classe Constraint exige que vous remplaciez Matches et WriteDescriptionTo (dans le cas d'une discordance, un récit pour la valeur attendue) mais également que vous remplaciez WriteActualValueTo (récit de la valeur réelle):

public class PortfolioEqualityConstraint : Constraint
{
    Portfolio expected;
    string expectedMessage = "";
    string actualMessage = "";

    public PortfolioEqualityConstraint(Portfolio expected)
    {
        this.expected = expected;
    }

    public override bool Matches(object actual)
    {
        if ( actual == null && expected == null ) return true;
        if ( !(actual is Portfolio) )
        { 
            expectedMessage = "<Portfolio>";
            actualMessage = "null";
            return false;
        }
        return Matches((Portfolio)actual);
    }

    private bool Matches(Portfolio actual)
    {
        if ( expected == null && actual != null )
        {
            expectedMessage = "null";
            expectedMessage = "non-null";
            return false;
        }
        if ( ReferenceEquals(expected, actual) ) return true;

        if ( !( expected.Property1.Equals(actual.Property1)
                 && expected.Property2.Equals(actual.Property2) 
                 && expected.Property3.Equals(actual.Property3) ) )
        {
            expectedMessage = expected.ToStringForTest();
            actualMessage = actual.ToStringForTest();
            return false;
        }
        return true;
    }

    public override void WriteDescriptionTo(MessageWriter writer)
    {
        writer.WriteExpectedValue(expectedMessage);
    }
    public override void WriteActualValueTo(MessageWriter writer)
    {
        writer.WriteExpectedValue(actualMessage);
    }
}

Plus la classe d'assistance:

public static class PortfolioState
{
    public static PortfolioEqualityConstraint Matches(Portfolio expected)
    {
        return new PortfolioEqualityConstraint(expected);
    }

    public static string ToStringForTest(this Portfolio source)
    {
        return String.Format("Property1 = {0}, Property2 = {1}, Property3 = {2}.", 
            source.Property1, source.Property2, source.Property3 );
    }
}

Exemple d'utilisation:

[TestFixture]
class PortfolioTests
{
    [Test]
    public void TestPortfolioEquality()
    {
        Portfolio LeftObject 
            = Portfolio.GetDefault();
        Portfolio RightObject 
            = Portfolio.GetPortfolio("{{GNOME}}", "{{NONE}}", "{{NONE}}");

        Assert.That( LeftObject, PortfolioState.Matches( RightObject ) );
    }
}
1
onedaywhen

Je voudrais construire sur la réponse de @Juanma. Cependant, je pense que cela ne devrait pas être mis en œuvre avec des assertions de tests unitaires. C'est un utilitaire qui pourrait très bien être utilisé dans certaines circonstances par un code non test. 

J'ai écrit un article sur le sujet http://timoch.com/blog/2013/06/unit-test-equality-is-not-domain-equality/

Ma proposition est la suivante:

/// <summary>
/// Returns the names of the properties that are not equal on a and b.
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns>An array of names of properties with distinct 
///          values or null if a and b are null or not of the same type
/// </returns>
public static string[] GetDistinctProperties(object a, object b) {
    if (object.ReferenceEquals(a, b))
        return null;
    if (a == null)
        return null;
    if (b == null)
        return null;

    var aType = a.GetType();
    var bType = b.GetType();

    if (aType != bType)
        return null;

    var props = aType.GetProperties();

    if (props.Any(prop => prop.GetIndexParameters().Length != 0))
        throw new ArgumentException("Types with index properties not supported");

    return props
        .Where(prop => !Equals(prop.GetValue(a, null), prop.GetValue(b, null)))
        .Select(prop => prop.Name).ToArray();
} 

Utiliser ceci avec NUnit

Expect(ReflectionUtils.GetDistinctProperties(tile, got), Empty);

renvoie le message suivant en cas de non concordance.

Expected: <empty>
But was:  < "MagmaLevel" >
at NUnit.Framework.Assert.That(Object actual, IResolveConstraint expression, String message, Object[] args)
at Undermine.Engine.Tests.TileMaps.BasicTileMapTests.BasicOperations() in BasicTileMapTests.cs: line 29
1
TiMoch

Regardez le lien suivant. C'est une solution de projet de code et je l'ai aussi utilisée. Cela fonctionne bien pour comparer les objets.

http://www.codeproject.com/Articles/22709/Testing-Equality-of-Two-Objects?msg=5189539#xx5189539xx

1
kanika

Je sais que la question est très ancienne, mais NUnit n’a toujours pas de support natif pour cela. Toutefois, si vous aimez les tests de style BDD (comme Jasmine), vous seriez agréablement surpris par NExpect ( https://github.com/fluffynuts/NExpect , fourni par NuGet), qui propose des tests d’égalité profonds. cuit juste là.

(disclaimer: je suis l'auteur de NExpect)

0
daf

Désérialisez les deux classes et effectuez une comparaison de chaîne.

EDIT: Fonctionne parfaitement, c'est le résultat obtenu de NUnit;

Test 'Telecom.SDP.SBO.App.Customer.Translator.UnitTests.TranslateEaiCustomerToDomain_Tests.TranslateNew_GivenEaiCustomer_ShouldTranslateToDomainCustomer_Test("ApprovedRatingInDb")' failed:
  Expected string length 2841 but was 5034. Strings differ at index 443.
  Expected: "...taClasses" />\r\n  <ContactMedia />\r\n  <Party i:nil="true" /..."
  But was:  "...taClasses" />\r\n  <ContactMedia>\r\n    <ContactMedium z:Id="..."
  ----------------------------------------------^
 TranslateEaiCustomerToDomain_Tests.cs(201,0): at Telecom.SDP.SBO.App.Customer.Translator.UnitTests.TranslateEaiCustomerToDomain_Tests.Assert_CustomersAreEqual(Customer expectedCustomer, Customer actualCustomer)
 TranslateEaiCustomerToDomain_Tests.cs(114,0): at Telecom.SDP.SBO.App.Customer.Translator.UnitTests.TranslateEaiCustomerToDomain_Tests.TranslateNew_GivenEaiCustomer_ShouldTranslateToDomainCustomer_Test(String custRatingScenario)

EDIT TWO: Les deux objets peuvent être identiques, mais l'ordre dans lequel les propriétés sont sérialisées n'est pas le même. Par conséquent, le XML est différent. DOH!

EDIT TROIS: .__ Cela fonctionne. Je l'utilise dans mes tests. Mais vous devez ajouter des éléments aux propriétés de la collection dans l'ordre dans lequel le code sous test les ajoute.

0
Casey Burns

J'ai fini par écrire une usine d'expression simple:

public static class AllFieldsEqualityComprision<T>
{
    public static Comparison<T> Instance { get; } = GetInstance();

    private static Comparison<T> GetInstance()
    {
        var type = typeof(T);
        ParameterExpression[] parameters =
        {
            Expression.Parameter(type, "x"),
            Expression.Parameter(type, "y")
        };
        var result = type.GetProperties().Aggregate<PropertyInfo, Expression>(
            Expression.Constant(true),
            (acc, prop) =>
                Expression.And(acc,
                    Expression.Equal(
                        Expression.Property(parameters[0], prop.Name),
                        Expression.Property(parameters[1], prop.Name))));
        var areEqualExpression = Expression.Condition(result, Expression.Constant(0), Expression.Constant(1));
        return Expression.Lambda<Comparison<T>>(areEqualExpression, parameters).Compile();
    }
}

et juste l'utiliser:

Assert.That(
    expectedCollection, 
    Is.EqualTo(actualCollection)
      .Using(AllFieldsEqualityComprision<BusinessCategoryResponse>.Instance));

C'est très utile puisque je dois comparer la collection de tels objets. Et vous pouvez utiliser cette comparaison ailleurs :)

Voici Gist avec l'exemple: https://Gist.github.com/Pzixel/b63fea074864892f9aba8ffde312094f

0
Alex Zhukovskiy

Stringify et comparer deux chaînes

Assert.AreEqual (JSON.stringify (LeftObject), JSON.stringify (RightObject))

0
jmtt89