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

发布于 2024-07-10 10:24:31 字数 476 浏览 9 评论 0原文

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

这些对象只是具有一堆公共属性的类的实例。 有没有一种简单的方法可以让 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 验证两个集合的内容是否相同。

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?

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(20

痴情换悲伤 2024-07-17 10:24:31

不要仅出于测试目的而覆盖 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 });
}

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

Use JSON to compare the object's data

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.

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.

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 });
}
嘿哥们儿 2024-07-17 10:24:31

尝试 FluentAssertions 库:

dto.Should().BeEquivalentTo(customer) 

它也可以使用 NuGet 安装。

Try FluentAssertions library:

dto.Should().BeEquivalentTo(customer) 

It can also be installed using NuGet.

鲜血染红嫁衣 2024-07-17 10:24:31

如果出于任何原因无法重写 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]);
    }
}

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:

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]);
    }
}
无风消散 2024-07-17 10:24:31

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

Assert.AreEqual(LeftObject, RightObject);

当然,这可能意味着您只需将所有单独的比较移至 .Equals 方法,但它允许您在多个测试中重用该实现,如果对象应该能够将自己与兄弟姐妹进行比较,那么这可能是有意义的。

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

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.

破晓 2024-07-17 10:24:31

我不想仅仅为了启用测试而重写 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.");
    }
}

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.

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:

// 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.");
    }
}
国粹 2024-07-17 10:24:31

我尝试了这里提到的几种方法。 大多数涉及序列化对象并进行字符串比较。 虽然超级简单并且通常非常有效,但我发现当您遇到失败并且报告类似这样的事情时,它会有点短:

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,稍后您会很高兴(如果您对此表示赞同,请也投票 < a href="https://stackoverflow.com/a/7440471/62600">dkl 的答案,其中首次建议使用 FluentAssertions)。

I've tried several approaches mentioned here. Most involve serializing your objects and doing a string compare. While super easy and generally very effective, I've found it comes up a little short when you have a failure and something like this gets reported:

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

Figuring out where where the differences are is a pain to say the least.

With FluentAssertions' object graph comparisons (i.e. a.ShouldBeEquivalentTo(b)), you get this back:

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

That's much nicer. Get FluentAssertions now, you'll be glad later (and if you upvote this, please also upvote dkl's answer where FluentAssertions was first suggested).

回梦 2024-07-17 10:24:31

我同意 ChrisYoxall 的观点——纯粹出于测试目的在主代码中实现 Equals 是不好的。

如果您实现 Equals 是因为某些应用程序逻辑需要它,那么这很好,但请保留纯测试代码以免混乱(而且检查相同测试的语义可能与您的应用程序所需的不同)。

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

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

狡猾

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

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

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

娇妻 2024-07-17 10:24:31

属性约束,在 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);

Property constraints, added in NUnit 2.4.2, allow a solution that is more readable than the OP's original one, and it produces much better failure messages. It's not in any way generic, but if you don't need to do it for too many classes, it's a very adequate solution.

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

Not as general-purpose as implementing Equals but it does give a much better failure message than

Assert.AreEqual(ExpectedObject, ActualObject);
烟火散人牵绊 2024-07-17 10:24:31

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

帮助方法:

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

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

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...

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.

时光匆匆的小流年 2024-07-17 10:24:31

只需安装 Nuget 的 ExpectedObjects,您就可以轻松比较两个对象的属性值、集合的每个对象值、两个组合对象的值以及匿名类型的部分比较属性值。

我在 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. ExpectedObjects 简介

Just install ExpectedObjects from Nuget, you can easily compare two objects's property value, each object value of collection, two composed object's value and partial compare property value by anonymous type.

I have some examples on github: https://github.com/hatelove/CompareObjectEquals

Here were some examples that contain scenarios of comparing object:

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

Reference:

  1. ExpectedObjects github
  2. Introduction of ExpectedObjects
沧笙踏歌 2024-07-17 10:24:31

这是一个非常古老的线程,但我想知道是否有原因没有提出答案 NUnit.Framework.Is.EqualToNUnit.Framework.Is.NotEqualTo

如:

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

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

This is a pretty old thread but I was wondering if there's a reason why no answer proposed NUnit.Framework.Is.EqualTo and NUnit.Framework.Is.NotEqualTo?

Such as:

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

and

Assert.That(LeftObject, Is.Not.EqualTo(RightObject)); 
转身泪倾城 2024-07-17 10:24:31

另一种选择是通过实现 NUnit 抽象 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 类的约定要求重写 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);
    }
}

加上辅助类:

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

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

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:

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 Constraint class requires one to override Matches and WriteDescriptionTo (in the case of a mismatch, a narrative for the expected value) but also overriding WriteActualValueTo (narrative for actual value) makes sense:

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 ) );
    }
}
茶底世界 2024-07-17 10:24:31

我会以@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

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.

I wrote an article on the matter 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

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
复古式 2024-07-17 10:24:31

https://github.com/kbilsted/StatePrinter 专门用于将对象图转储为字符串表示形式编写简单的单元测试的目的。

  • 它带有断言方法,可以输出正确转义的字符串,轻松复制粘贴到测试中以纠正它。
  • 它允许自动重写单元测试
  • 它与所有单元测试框架集成
  • 与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);

https://github.com/kbilsted/StatePrinter has been written specifically to dump object graphs to string representation with the aim of writing easy unit tests.

  • It comes witg Assert methods that output a properly escaped string easy copy-paste into the test to correct it.
  • It allows unittest to be automatically re-written
  • It integrates with all unit testing frameworks
  • Unlike JSON serialization, circular references are supported
  • You can easily filter, so only parts of types are dumped

Given

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

You can in a type safe manner, and using auto-completion of visual studio include or exclude fields.

  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);
一指流沙 2024-07-17 10:24:31

我最终编写了一个简单的表达式工厂:

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

I've ended with writing a simple expression 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();
    }
}

and just use it:

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

It's very useful since I have to compare collection of such objects. And you can use this comparere somewhere else :)

Here is gist with example: https://gist.github.com/Pzixel/b63fea074864892f9aba8ffde312094f

最初的梦 2024-07-17 10:24:31

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

编辑:
工作完美,这是我从 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 是不同的。 卫生部!

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

Deserialize both classes, and do a string compare.

EDIT:
Works perfectly, this is the output I get from 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!

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.

倾听心声的旋律 2024-07-17 10:24:31

我知道这是一个非常老的问题,但 NUnit 仍然没有对此提供本机支持。 然而,如果您喜欢 BDD 风格的测试(ala Jasmine),您会对 NExpect 感到惊喜(https:/ /github.com/fluffynuts/NExpect,从 NuGet 获取),其中包含深度平等测试。

(免责声明:我是 NExpect 的作者)

I know this is a really old question, but NUnit still doesn't have native support for this. However, if you like BDD-style testing (ala Jasmine), you'd be pleasantly surprised with NExpect (https://github.com/fluffynuts/NExpect, get it from NuGet), which has deep equality testing baked right in there.

(disclaimer: I am the author of NExpect)

漫漫岁月 2024-07-17 10:24:31

这只是上述答案之一的修改版本,可与 Moq 一起使用:

public static class Helpers {

    public static bool DeepCompare(this object actual, object expected) {
        var properties = expected.GetType().GetProperties();
        foreach (var property in properties) {
            var expectedValue = property.GetValue(expected, null);
            var actualValue = property.GetValue(actual, null);

            if (actualValue == null && expectedValue == null) {
                return true;
            }

            if (actualValue == null || expectedValue == null) {
                return false;
            }

            if (actualValue is IList actualList) {
                if (!AreListsEqual(actualList, (IList)expectedValue)) {
                    return false;
                }
            }
            else if (IsValueType(expectedValue)) {
                if(!Equals(expectedValue, actualValue)) {
                    return false;
                }
            }
            else if (expectedValue is string) {
                return actualValue is string && Equals(expectedValue, actualValue);
            }
            else if (!DeepCompare(expectedValue, actualValue)) {
                return false;
            }
                
        }
        return true;
    }

    private static bool AreListsEqual(IList actualList, IList expectedList) {
        if (actualList == null && expectedList == null) {
            return true;
        }

        if (actualList == null  || expectedList == null) {
            return false;
        }

        if (actualList.Count != expectedList.Count) {
            return false;
        }

        if (actualList.Count == 0) {
            return true;
        }

        var isValueTypeOrString = IsValueType(actualList[0]) || actualList[0] is string;

        if (isValueTypeOrString) {
            for (var i = 0; i < actualList.Count; i++) {
                if (!Equals(actualList[i], expectedList[i])) {
                    return false;
                }
            }
        }
        else {
            for (var i = 0; i < actualList.Count; i++) {
                if (!DeepCompare(actualList[i], expectedList[i])) {
                    return false;
                }
            }
        }

        return true;
    }

    private static bool IsValueType(object obj) {
        return obj != null && obj.GetType().IsValueType;
    }

当您需要的内容不只是 It.IsAny<> 时,它可用于在模拟类型上指定设置时匹配对象。 并希望匹配所有属性,如下所示:

_clientsMock.Setup(m => m.SearchClients(
            It.Is<SearchClientsPayload>(x => x.DeepCompare(expectedRequest)))).Returns(expectedResponse);

当然可以改进它以处理可枚举和其他复杂场景。

Here is just a modified version of one of the answers above that can be used with Moq:

public static class Helpers {

    public static bool DeepCompare(this object actual, object expected) {
        var properties = expected.GetType().GetProperties();
        foreach (var property in properties) {
            var expectedValue = property.GetValue(expected, null);
            var actualValue = property.GetValue(actual, null);

            if (actualValue == null && expectedValue == null) {
                return true;
            }

            if (actualValue == null || expectedValue == null) {
                return false;
            }

            if (actualValue is IList actualList) {
                if (!AreListsEqual(actualList, (IList)expectedValue)) {
                    return false;
                }
            }
            else if (IsValueType(expectedValue)) {
                if(!Equals(expectedValue, actualValue)) {
                    return false;
                }
            }
            else if (expectedValue is string) {
                return actualValue is string && Equals(expectedValue, actualValue);
            }
            else if (!DeepCompare(expectedValue, actualValue)) {
                return false;
            }
                
        }
        return true;
    }

    private static bool AreListsEqual(IList actualList, IList expectedList) {
        if (actualList == null && expectedList == null) {
            return true;
        }

        if (actualList == null  || expectedList == null) {
            return false;
        }

        if (actualList.Count != expectedList.Count) {
            return false;
        }

        if (actualList.Count == 0) {
            return true;
        }

        var isValueTypeOrString = IsValueType(actualList[0]) || actualList[0] is string;

        if (isValueTypeOrString) {
            for (var i = 0; i < actualList.Count; i++) {
                if (!Equals(actualList[i], expectedList[i])) {
                    return false;
                }
            }
        }
        else {
            for (var i = 0; i < actualList.Count; i++) {
                if (!DeepCompare(actualList[i], expectedList[i])) {
                    return false;
                }
            }
        }

        return true;
    }

    private static bool IsValueType(object obj) {
        return obj != null && obj.GetType().IsValueType;
    }

It can be used to match an object when specifying a setup on a mocked type when you need something more than It.IsAny<> and want to match on all properties, like this:

_clientsMock.Setup(m => m.SearchClients(
            It.Is<SearchClientsPayload>(x => x.DeepCompare(expectedRequest)))).Returns(expectedResponse);

It can of course be improved to work with enumerables and other complex scenarios.

淡淡的优雅 2024-07-17 10:24:31

Compare-Net-Objects 项目已内置于 测试扩展 支持比较 NUnit 中的嵌套对象。

using KellermanSoftware.CompareNetObjects;

[Test]
public void ShouldCompare_When_Equal_Should__Not_Throw_An_Exception()
{
    //Arrange
    string errorMessage = "Groups should be equal";
    var people1 = new List<Person>() { new Person() { Name = "Joe" } };
    var people2 = new List<Person>() { new Person() { Name = "Joe" } };
    var group1 = new KeyValuePair<string, List<Person>>("People", people1);
    var group2 = new KeyValuePair<string, List<Person>>("People", people2);

    //Assert
    group1.ShouldCompare(group2, errorMessage);
}

The Compare-Net-Objects project has built in test extensions to support comparing nested objects within NUnit.

using KellermanSoftware.CompareNetObjects;

[Test]
public void ShouldCompare_When_Equal_Should__Not_Throw_An_Exception()
{
    //Arrange
    string errorMessage = "Groups should be equal";
    var people1 = new List<Person>() { new Person() { Name = "Joe" } };
    var people2 = new List<Person>() { new Person() { Name = "Joe" } };
    var group1 = new KeyValuePair<string, List<Person>>("People", people1);
    var group2 = new KeyValuePair<string, List<Person>>("People", people2);

    //Assert
    group1.ShouldCompare(group2, errorMessage);
}
·深蓝 2024-07-17 10:24:31

对两个字符串进行字符串化和比较

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

Stringify and compare two strings

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

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文