C# 比较 NUnit 中两个对象之间的相等性

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/318210/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-03 22:59:25  来源:igfitidea点击:

Compare equality between two objects in NUnit

c#unit-testingautomated-testsnunit

提问by Michael Haren

I'm trying to assert that one object is "equal" to another object.

我试图断言一个对象与另一个对象“相等”。

The objects are just instances of a class with a bunch of public properties. Is there an easy way to have NUnit assert equality based on the properties?

对象只是具有一堆公共属性的类的实例。有没有一种简单的方法可以让 NUnit 根据属性断言相等?

This is my current solution but I think there may be something better:

这是我目前的解决方案,但我认为可能有更好的方法:

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

What I'm going for would be in the same spirit as the CollectionEquivalentConstraint wherein NUnit verifies that the contents of two collections are identical.

我想要的将与 CollectionEquivalentConstraint 的精神相同,其中 NUnit 验证两个集合的内容是否相同。

采纳答案by Lasse V. Karlsen

Override .Equals for your object and in the unit test you can then simply do this:

覆盖 .Equals 为您的对象,然后在单元测试中,您可以简单地执行以下操作:

Assert.AreEqual(LeftObject, RightObject);

Of course, this might mean you just move all the individual comparisons to the .Equals method, but it would allow you to reuse that implementation for multiple tests, and probably makes sense to have if objects should be able to compare themselves with siblings anyway.

当然,这可能意味着您只需将所有单独的比较移动到 .Equals 方法,但它允许您对该实现重复使用多个测试,并且如果对象无论如何都应该能够将自己与同级进行比较可能是有意义的。

回答by Juanma

If you can't override Equals for any reason, you can build a helper method that iterates through public properties by reflection and assert each property. Something like this:

如果由于任何原因无法覆盖 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]);
    }
}

回答by Chris Yoxall

I prefer not to override Equals just to enable testing. Don't forget that if you do override Equals you really should override GetHashCode also or you may get unexpected results if you are using your objects in a dictionary for example.

我不想为了启用测试而覆盖 Equals。不要忘记,如果您确实覆盖了 Equals,则您确实也应该覆盖 GetHashCode,否则如果您在字典中使用对象,则可能会得到意想不到的结果。

I do like the reflection approach above as it caters for the addition of properties in the future.

我确实喜欢上面的反射方法,因为它可以满足将来添加属性的需求。

For a quick and simple solution however its often easiest to either create a helper method that tests if the objects are equal, or implement IEqualityComparer on a class you keep private to your tests. When using IEqualityComparer solution you dont need to bother with the implementation of GetHashCode. For example:

然而,对于快速简单的解决方案,通常最简单的方法是创建一个辅助方法来测试对象是否相等,或者在您对测试保密的类上实现 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.");
    }
}

回答by Sly Gryphon

I agree with ChrisYoxall -- implementing Equals in your main code purely for testing purposes is not good.

我同意 ChrisYoxall——在你的主代码中实现 Equals 纯粹用于测试目的并不好。

If you are implementing Equals because some application logic requires it, then that's fine, but keep pure testing-only code out of cluttering up stuff (also the semantics of checking the same for testing may be different than what your app requires).

如果您正在实施 Equals 因为某些应用程序逻辑需要它,那么这很好,但要避免将纯测试代码弄得乱七八糟(检查相同内容以进行测试的语义可能与您的应用程序要求的不同)。

In short, keep testing-only code out of your class.

简而言之,将仅用于测试的代码排除在类之外。

Simple shallow comparison of properties using reflection should be enough for most classes, although you may need to recurse if your objects have complex properties. If following references, beware of circular references or similar.

对于大多数类,使用反射对属性进行简单的浅层比较应该就足够了,尽管如果您的对象具有复杂的属性,您可能需要递归。如果遵循引用,请注意循环引用或类似引用。

Sly

狡猾

回答by Casey Burns

Deserialize both classes, and do a string compare.

反序列化两个类,并进行字符串比较。

EDIT:Works perfectly, this is the output I get from NUnit;

编辑:完美运行,这是我从 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:The two objects can be identical, but the order that properties are serialized in are not the same. Therefore the XML is different. DOH!

编辑二:这两个对象可以相同,但属性序列化的顺序不同。因此 XML 是不同的。哦!

EDIT THREE:This does work. I am using it in my tests. But you must add items to collection properties in the order the code under test adds them.

编辑三:这确实有效。我在我的测试中使用它。但是您必须按照被测代码添加项目的顺序将项目添加到集合属性。

回答by dkl

Try FluentAssertions library:

试试 FluentAssertions 库:

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

http://www.fluentassertions.com/

http://www.fluentassertions.com/

It can also be installed using NuGet.

它也可以使用 NuGet 安装。

回答by Max

Do not override Equals just for testing purposes. It's tedious and affects domain logic. Instead,

不要仅出于测试目的而覆盖 Equals。它很乏味并且会影响域逻辑。反而,

Use JSON to compare the object's data

使用 JSON 比较对象的数据

No additional logic on your objects. No extra tasks for testing.

您的对象没有额外的逻辑。没有额外的测试任务。

Just use this simple method:

只需使用这个简单的方法:

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

It seems to work out great. The test runner results info will show the JSON string comparison (the object graph) included so you see directly what's wrong.

看起来效果很好。测试运行器结果信息将显示包含的 JSON 字符串比较(对象图),以便您直接查看问题所在。

Also note!If you have bigger complex objects and just want to compare parts of them you can (use LINQ for sequence data) create anonymous objects to use with above method.

还要注意!如果您有更大的复杂对象并且只想比较它们的一部分,您可以(对序列数据使用 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 });
}

回答by onedaywhen

Another option is to write a custom constraint by implementing the NUnit abstract Constraintclass. With a helper class to provide a little syntactic sugar, the resulting test code is pleasantly terse and readable e.g.

另一种选择是通过实现 NUnit 抽象Constraint类来编写自定义约束。使用辅助类提供一点语法糖,生成的测试代码简洁易读,例如

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

For an extreme example, consider class which has 'read-only' members, is not IEquatable, and you could not change the class under test even if you wanted to:

举一个极端的例子,考虑具有“只读”成员IEquatable的类is not ,即使您想更改被测类,您也无法更改:

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

The contract for the Constraintclass requires one to override Matchesand WriteDescriptionTo(in the case of a mismatch, a narrative for the expected value) but also overriding WriteActualValueTo(narrative for actual value) makes sense:

对于合同Constraint类需要一个覆盖MatchesWriteDescriptionTo(在不匹配,对预期值叙述的情况下),而且还覆盖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);
    }
}

Plus the helper class:

加上助手类:

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

Example usage:

用法示例:

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

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

回答by samaspin

Max Wikstrom's JSON solution (above) makes the most sense to me, it's short, clean and most importantly it works. Personally though I'd prefer to implement the JSON conversion as a separate method and place the assert back inside the unit test like this...

Max Wikstrom 的 JSON 解决方案(上图)对我来说最有意义,它简短、干净,最重要的是它有效。虽然我个人更喜欢将 JSON 转换实现为单独的方法,并将断言放回单元测试中,如下所示......

HELPER METHOD:

辅助方法:

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 - You may need to add a reference to System.Web.Extensions in your solution.

仅供参考 - 您可能需要在解决方案中添加对 System.Web.Extensions 的引用。

回答by TiMoch

I would build on the answer of @Juanma. However, I believe this should not be implemented with unit test assertions. This is a utility that could very well be used in some circumstances by non-test code.

我会以@Juanma 的答案为基础。但是,我认为这不应该用单元测试断言来实现。这是一个实用程序,可以在某些情况下由非测试代码很好地使用。

I wrote an article on the matter http://timoch.com/blog/2013/06/unit-test-equality-is-not-domain-equality/

我写了一篇关于此事的文章http://timoch.com/blog/2013/06/unit-test-equality-is-not-domain-equality/

My proposal is as follow:

我的建议如下:

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

Using this with NUnit

将此与 NUnit 一起使用

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

yields the following message on mismatch.

在不匹配时产生以下消息。

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