Сравните равенство между двумя объектами в NUnit



Я пытаюсь утверждать, что один объект "равен" другому объекту.



объекты-это просто экземпляры класса с кучей общедоступных свойств. Есть ли простой способ заставить NUnit утверждать равенство на основе свойств?



Это мое текущее решение, но я думаю, что там может быть что-то лучше:



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


то, что я собираюсь сделать, будет в том же духе, что и CollectionEquivalentConstraint, в котором NUnit проверяет, что содержимое двух коллекции идентичны.

757   19  

19 ответов:

переопределить .Равно для вашего объекта и в модульном тесте вы можете просто сделать это:

Assert.AreEqual(LeftObject, RightObject);

конечно, это может означать, что вы просто переместить все индивидуальные сравнения .Метод Equals, но он позволит вам повторно использовать эту реализацию для нескольких тестов и, вероятно, имеет смысл иметь, если объекты должны иметь возможность сравнивать себя с братьями и сестрами в любом случае.

Если вы не можете переопределить Equals по какой-либо причине, вы можете построить вспомогательный метод, который выполняет итерацию по общедоступным свойствам путем отражения и утверждает каждое свойство. Что-то вроде этого:

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]);
    }
}

не переопределить Equals только для целей тестирования. Это утомительно и влияет на логику домена. Вместо этого,

используйте JSON для сравнения данных объекта

нет дополнительной логики на ваших объектах. Никаких дополнительных заданий для тестирования.

просто используйте этот простой способ:

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);
}

это, кажется, работает отлично. Информация о результатах тестового запуска покажет сравнение строк JSON (график объектов), включенное, чтобы вы напрямую видели, что такое неправильный.

кроме того, обратите внимание! если у вас есть большие сложные объекты и просто хотите сравнить их части, вы можете (используйте LINQ для данных последовательности) создание анонимных объектов для использования с вышеуказанным методом.

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 });
}

попробуйте библиотеку FluentAssertions:

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

http://www.fluentassertions.com/

Он также может быть установлен с помощью NuGet.

Я предпочитаю не переопределить Equals для тестирования. Не забывайте, что если вы переопределяете Equals, вы действительно должны переопределить GetHashCode, или вы можете получить неожиданные результаты, если вы используете свои объекты в словаре, например.

Мне нравится подход отражения выше, поскольку он обслуживает добавление свойств в будущем.

для быстрого и простого решения, однако его часто легче создать вспомогательный метод, который проверяет, если объекты равны или реализуют IEqualityComparer в классе, который вы держите в секрете для своих тестов. При использовании решения IEqualityComparer вам не нужно беспокоиться о реализации GetHashCode. Например:

// 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.");
    }
}

Я пробовал несколько подходов, упомянутых здесь. Большинство из них включают в себя сериализацию объектов и выполнение сравнения строк. В то время как супер легко и в целом очень эффективно, я обнаружил, что он приходит немного короче, когда у вас есть сбой и что-то вроде этого сообщается:

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

выяснить, где, где различия-это боль, чтобы не сказать больше.

С FluentAssertions' сравнение графов объектов (т. е. a.ShouldBeEquivalentTo(b)), ты получишь это назад:

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

Это гораздо приятнее. Получить FluentAssertions теперь вы будете рады позже (и если вы повысите это, пожалуйста, также повысьте ответ дкл где впервые был предложен FluentAssertions).

Я согласен с ChrisYoxall -- реализация Equals в вашем основном коде исключительно для целей тестирования не очень хороша.

Если вы реализуете Equals, потому что для этого требуется некоторая логика приложения, то это нормально, но держите чистый тестовый код только из загромождающего материала (также семантика проверки того же для тестирования может отличаться от того, что требуется вашему приложению).

короче говоря, продолжайте тестировать только код из вашего класса.

простое мелкое сравнение свойств, использующих отражение, должно быть достаточно для большинства классов, хотя вам может потребоваться рекурсия, если ваши объекты имеют сложные свойства. Если следующие ссылки, остерегайтесь круговых ссылок или аналогичных.

хитрый

ограничения собственность, добавлено в NUnit 2.4.2, разрешить решение, которое является более читаемым, чем исходный OP, и он производит гораздо лучшие сообщения об ошибках. Это ни в коем случае не общий, но если вам не нужно делать это для слишком многих классов, это очень адекватное решение.

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

не так универсально, как реализация Equals но это дает гораздо лучшее сообщение об ошибке, чем

Assert.AreEqual(ExpectedObject, ActualObject);

решение JSON Max Wikstrom (выше) имеет для меня наибольший смысл, оно короткое, чистое и, самое главное, оно работает. Лично я бы предпочел реализовать преобразование JSON как отдельный метод и поместить assert обратно в модульный тест, как это...

ВСПОМОГАТЕЛЬНЫЙ МЕТОД:

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

МОДУЛЬНЫЙ ТЕСТ:

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-возможно, Вам потребуется добавить ссылку на систему.Сеть.Расширения в вашем решении.

другой вариант-написать пользовательское ограничение, реализовав NUnit abstract Constraint класса. С помощью вспомогательного класса, чтобы обеспечить немного синтаксического сахара, полученный тестовый код приятно лаконичен и читаем, например

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

для крайнего примера рассмотрим класс, который имеет члены только для чтения, не IEquatable, и вы не можете изменить класс под тестом, даже если вы хотите:

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}}" 
        };
    }
}

в договоре Constraint класс требует переопределить Matches и WriteDescriptionTo (в случае несоответствия, повествование для ожидаемого значения) , но и переопределение WriteActualValueTo (повествование для фактического значения) имеет смысл:

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);
    }
}

плюс вспомогательный класс:

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 );
    }
}

пример использования:

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

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

Я бы построил на ответе @Juanma. Однако я считаю, что это не должно быть реализовано с помощью утверждений модульного теста. Это утилита, которая очень хорошо может быть использована в некоторых случаях не тестовым кодом.

Я написал статью по этому вопросу http://timoch.com/blog/2013/06/unit-test-equality-is-not-domain-equality/

мое предложение выглядит следующим образом:

/// <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();
} 

используя это с NUnit

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

дает следующие сообщение о несоответствии.

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

https://github.com/kbilsted/StatePrinter был написан специально для дампа графов объектов в строковое представление с целью написания простых модульных тестов.

  • он приходит с помощью методов утверждения, которые выводят правильно экранированную строку, легко копируют-вставляют в тест, чтобы исправить ее.
  • он позволяет unittest автоматически переписываться
  • он интегрируется со всеми платформами модульного тестирования
  • в отличие от сериализации JSON, циклические ссылки поддерживаются
  • вы можете легко фильтровать, поэтому сбрасываются только части типов

дано

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

вы можете в типобезопасном порядке и с помощью автоматического завершения visual studio включить или исключить поля.

  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);

просто установите ExpectedObjects из Nuget, вы можете легко сравнить значение свойства двух объектов, каждое значение объекта коллекции, значение двух составных объектов и частичное сравнение значения свойства по анонимному типу.

У меня есть несколько примеров на github:https://github.com/hatelove/CompareObjectEquals

вот несколько примеров, которые содержат сценарии сравнения объект:

    [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);
    }

ссылки:

  1. ExpectedObjects github
  2. введение ожидаемых объектов

посмотрите на следующую ссылку. Его решение из проекта кода, и я использовал его тоже. Он отлично работает для сравнения объектов.

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

Это довольно старая тема, но мне было интересно, есть ли причина, по которой нет ответа, предложенного NUnit.Framework.Is.EqualTo и NUnit.Framework.Is.NotEqualTo?

, например:

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

и

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

Десериализуйте оба класса и выполните сравнение строк.

EDIT: Работает отлично, это выход, который я получаю от 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)

РЕДАКТИРОВАТЬ ДВА: Эти два объекта могут быть идентичны, но порядок, в котором сериализуются свойства, не является одинаковым. Поэтому XML отличается. Дох!

РЕДАКТИРОВАТЬ ТРИ: Это действительно работает. Я использую его в своих тестах. Но вы должны добавить элементы в свойства коллекции в порядке тестируемый код добавляет их.

stringify и сравнить две строки

утверждать.AreEqual(JSON.преобразовать в строки(LeftObject), в формате JSON.stringify (RightObject))

Я закончил с написанием простой фабрики выражений:

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();
    }
}

и просто использовать это:

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

Это очень полезно, Так как я должен сравнить коллекция таких объектов. И вы можете использовать этот компаратор где-то еще :)

вот суть с примером:https://gist.github.com/Pzixel/b63fea074864892f9aba8ffde312094f

Я знаю, что это действительно старый вопрос, но у NUnit все еще нет родной поддержки для этого. Однако, если вам нравится тестирование BDD-стиля (ala Jasmine), вы будете приятно удивлены NExpect (https://github.com/fluffynuts/NExpect, получите его от NuGet), который имеет глубокое тестирование равенства, запеченное прямо там.

(отказ от ответственности: я являюсь автором NExpect)

Comments

    Ничего не найдено.