Подтвердить что ты не робот

Сравните равенство между двумя объектами в 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 проверяет, что содержимое двух коллекций идентично.

4b9b3361

Ответ 1

Переопределить .Equals для вашего объекта и в unit test, вы можете просто сделать это:

Assert.AreEqual(LeftObject, RightObject);

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

Ответ 2

Если вы не можете переопределить 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]);
    }
}

Ответ 3

Не переопределяйте 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 });
}

Ответ 4

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

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

http://www.fluentassertions.com/

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

Ответ 5

Я предпочитаю не переопределять 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.");
    }
}

Ответ 6

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

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, вы будете рады позже (и если вы повысите это значение, пожалуйста, также увеличьте dkl answer, где сначала предлагалось FluentAssertions).

Ответ 7

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

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

Короче говоря, держите только тестовый код вне своего класса.

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

Хитрый

Ответ 8

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

СПОСОБ ПОМОЩИ:

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

UNIT TEST:

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 - вам может потребоваться добавить ссылку на System.Web.Extensions в вашем решении.

Ответ 9

Ограничения свойств, добавленные в 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);

Ответ 10

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

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

Ответ 11

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

Я написал статью по этому вопросу 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

Ответ 12

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

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

Учитывая

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

Вы можете использовать безопасный тип и использовать автоматическое заполнение визуальных студий включите или исключите поля.

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

Ответ 13

Просто установите 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 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);
    }

Ссылка:

Ответ 14

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

Например:

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

и

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

Ответ 15

Deserialize обоих классов и сравните строку.

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 отличается. DOH!

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

Ответ 17

Строка и сравнение двух строк

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

Ответ 18

Я закончил писать простое выражение factory:

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