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

158

我想要验证一个对象是否“相等”,这些对象只是一个类的实例,拥有许多公共属性。有没有一种简单的方法使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验证两个集合的内容是否相同。


请参阅:https://github.com/GregFinzer/Compare-Net-Objects它包含测试扩展,可做您所需的内容:https://github.com/GregFinzer/Compare-Net-Objects/wiki/Test-Extensions - phoenix
20个回答

144

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

1
使用 Linq!@DmitryBLR(请查看答案中的最后一段):) - Max
3
这是一个很好的想法。我会使用更新的Json.NET:var expectedJson = Newtonsoft.Json.JsonConvert.SerializeObject(expected); - BrokeMyLegBiking
2
这个无法处理循环引用。请使用 https://github.com/kbilsted/StatePrinter/ 代替 JSON 方法,以获得更好的体验。 - Carlo V. Dango
2
这是正确的@KokaChernov,有时候您想要在排序不同的情况下失败测试,但如果您不想在排序不同的情况下失败,则可以在将它们传递给AreEqualByJson方法之前对列表进行显式排序(使用linq)。在测试之前“重新排列”对象的简单变体在答案中的最后一个代码示例中。所以我认为这非常“通用”! :) - Max
1
@MaxWikström确实如此,但是如果您没有明确的顺序并且没有介绍它的方法呢?目前我的noSQL存储库在子列表中返回混合顺序,因此,我可能可以使用linq并发明一些排序方式,但这并不是那么简单。所以,您看,我需要一些很酷的开箱即用的解决方案,而不是在自定义代码修补中瞎搞。无论如何,已点赞。 - Konstantin Chernov
显示剩余7条评论

122

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

@wesley:这不是真的。Type.GetProperties方法:返回当前类型的所有公共属性。请参阅http://msdn.microsoft.com/en-us/library/aky14axb.aspx。 - Sergii Volchkov
4
谢谢。然而,我不得不交换实参和期望参数的顺序,因为惯例是期望参数在实参之前。 - Valamas
1
这是我个人认为更好的方法,Equal和HashCode重写不应该基于比较每个字段,再加上在每个对象上进行这样的操作非常繁琐。干得好! - Scott White
3
如果您的类型只有基本类型作为属性,那么这个方法会很有效。但是,如果您的类型具有带有自定义类型的属性(而这些类型没有实现Equals方法),则该方法将失败。 - Bobby Cannon
添加了一些对象属性的递归,但我不得不跳过索引属性: - cerhart
这个答案中的代码是一个不错的起点,但无法处理不是ILists的可枚举对象或包含没有实现Equals的属性的对象。我对其进行了扩展以使其适用于这些情况:https://gist.github.com/rotoclone/7cf50cbbb974853070368942a9f475a7 - the_nacho

117

尝试使用FluentAssertions库:

dto.Should().BeEquivalentTo(customer) 

你也可以使用 NuGet 安装。


20
ShouldHave 已经被弃用,所以应该使用 dto.ShouldBeEquivalentTo(customer) 替代。 - WhiteKnight
3
基于这个原因,这是最佳答案。 - Todd Menier
2
ShouldBeEquivalent 有 bug :( - Konstantin Chernov
3
刚遇到同样的问题,使用以下代码似乎正常工作:actual.ShouldBeEquivalentTo(expected, x => x.ExcludingMissingMembers()) - dragonfly02
3
在版本5.10.3中,语法为dto.Should().BeEquivalentTo(customer)。 - NJS
显示剩余4条评论

53

为您的对象覆盖.Equals方法,并在单元测试中您就可以像这样简单地进行:

Assert.AreEqual(LeftObject, RightObject);

当然,这可能意味着你只是将所有的单个比较移动到.Equals方法中,但它将允许您重用该实现进行多个测试,并且如果对象本来就应该能够与同级对象进行比较,那么这样做可能是有意义的。


2
谢谢,lassevk。这对我有用!我根据这里的指南(http://msdn.microsoft.com/en-us/library/336aedhh(VS.80).aspx)实现了.Equals。 - Michael Haren
14
当然要包括 GetHashCode() 函数 ;-p - Marc Gravell
1
一个重要的注意事项:如果您的对象还实现了 IEnumerable 接口,则无论如何重写 Equals 方法,它都将被视为一个集合进行比较,因为 NUnit 给予 IEnumerable 更高的优先级。有关详细信息,请参阅 NUnitEqualityComparer.AreEqual 方法。您可以使用相等性约束的 Using() 方法之一来覆盖比较器。即使这样做,也不能满足非泛型 IEqualityComparer 的实现,因为 NUnit 使用了适配器。 - Kaleb Pederson
17
更多注意事项:在可变类型上实现 GetHashCode() 将会导致如果您将该对象用作键时出现错误。在我看来,仅为了测试而覆盖 Equals()GetHashCode() 并使对象不可变是没有意义的。 - bavaza

39

我不喜欢仅为了方便测试而覆盖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.");
    }
}

equals方法不能处理空值。在返回语句之前,我会添加以下内容到equals方法中。if (x == null && y == null) { return true; } if (x == null || y == null) { return false; } 我已经编辑了问题以添加空值支持。 - Bobby Cannon
在 GetHashCode 中使用 throw new NotImplementedException(); 对我没有用。为什么我无论如何都需要在 IEqualityComparer 中使用该函数? - love2code

27

我已经尝试了这里提到的几种方法。大多数都涉及序列化您的对象并进行字符串比较。虽然非常简单且通常非常有效,但我发现当出现故障并报告类似于以下内容时,它可能会不够用:

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的回答点个赞,因为是他首次提出了FluentAssertions)。


10

我同意ChrisYoxall的观点--仅出于测试目的在主代码中实现Equals并不好。

如果您正在实现Equals,是因为某些应用程序逻辑需要它,那么没问题,但请勿将纯测试代码混杂在一起(而且检查测试是否相同的语义可能与您的应用程序要求不同)。

简单的浅层属性比较使用反射应该足以满足大多数类的需求,尽管如果您的对象具有复杂属性,则可能需要递归。如果跟随引用,请注意循环引用或类似问题。

Sly


循环引用方面的处理得很好。如果您在比较树中保留一个对象字典,那么这将变得非常容易克服。 - Lucas B

8

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

4

只需从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库

这个解决方案很好,而且很容易实现。 - Ken Tseng

4

我认为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的引用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接