【问题标题】:C# - Asserting two objects are equal in unit testsC# - 在单元测试中断言两个对象相等
【发布时间】:2014-06-25 05:00:16
【问题描述】:

使用 Nunit 或 Microsoft.VisualStudio.TestTools.UnitTesting。现在我的断言失败了。

    [TestMethod]
    public void GivenEmptyBoardExpectEmptyBoard()
    {
        var test = new Board();

        var input = new Board()
            {
                Rows = new List<Row>()
                    {
                        new Row(){Cells = new List<int>(){0,0,0,0}},
                        new Row(){Cells = new List<int>(){0,0,0,0}},
                        new Row(){Cells = new List<int>(){0,0,0,0}},
                        new Row(){Cells = new List<int>(){0,0,0,0}},
                    }
            };

        var expected = new Board()
        {
            Rows = new List<Row>()
                    {
                        new Row(){Cells = new List<int>(){0,0,0,0}},
                        new Row(){Cells = new List<int>(){0,0,0,0}},
                        new Row(){Cells = new List<int>(){0,0,0,0}},
                        new Row(){Cells = new List<int>(){0,0,0,0}},
                    }
        };

        var lifeOrchestration = new LifeOrchestration();

        var actual = lifeOrchestration.Evolve(input);

        Assert.AreEqual(expected, actual);
    }

【问题讨论】:

标签: c# .net unit-testing tdd nunit


【解决方案1】:

Assert 方法依赖于对象的 Equals 和 GetHashcode。您可以实现它,但如果在单元测试之外不需要此对象相等性,我会考虑比较对象上的各个原始类型。 看起来对象很简单,并没有必要重写 equals。

【讨论】:

    【解决方案2】:

    您有两个不同的Board 实例,因此您对Assert.AreEqual 的调用将失败。即使它们的全部内容看起来相同,您也是在比较引用,而不是基础值。

    您必须指定使两个 Board 实例相等的原因。

    你可以在你的测试中做到这一点:

    Assert.AreEqual(expected.Rows.Count, actual.Rows.Count);
    Assert.AreEqual(expected.Rows[0].Cells[0], actual.Rows[0].Cells[0]);
    
    // Lots more tests of equality...
    

    或者你可以在你的课堂上这样做:(注意我是在运行中写的 - 你需要调整它)

    public class Board
    {
        public List<Row> Rows = new List<Row>();
    
        public override bool Equals(object obj)
        {
            var board = obj as Board;
    
            if (board == null)
                return false;
    
            if (board.Rows.Count != Rows.Count)
                return false;
    
            return !board.Rows.Where((t, i) => !t.Equals(Rows[i])).Any();
        }
    
        public override int GetHashCode()
        {
            // determine what's appropriate to return here - a unique board id may be appropriate if available
        }
    }
    
    public class Row
    {
        public List<int> Cells = new List<int>(); 
    
        public override bool Equals(object obj)
        {
            var row = obj as Row;
    
            if (row == null)
                return false;
    
            if (row.Cells.Count != Cells.Count)
                return false;
    
            if (row.Cells.Except(Cells).Any())
                return false;
    
            return true;
        }
    
        public override int GetHashCode()
        {
            // determine what's appropriate to return here - a unique row id may be appropriate if available
        }
    }
    

    【讨论】:

    • 虽然我几乎没有进行过 C# 开发,但我可以看到这种方法有效,但我使用的是 Enumerable.SequenceEquals(),这至少可以减少这个解决方案中的一些痛苦。
    • 我认为值得注意的是这种方法的一些缺点:它将单元测试代码粘贴到您的生产代码中,它会创建更多本身必须进行单元测试的生产代码,该代码可能会改变其他生产的行为代码,它并没有真正减少你需要的代码总量。
    【解决方案3】:

    我曾经重写 getHasCode 和 equals,但我从不喜欢它,因为我不想为了单元测试而更改我的生产代码。 也有点痛。

    然后我过于反思,无法比较侵入性较小的对象……但这需要大量工作(很多极端情况)

    最后我使用:

    http://www.nuget.org/packages/DeepEqual/ 效果很好。

    更新,6 年后

    我现在使用 .NET 的更通用的库 fluentassertions 它和上面一样,但是有更多的特性和一个不错的 DSL,具体的替换是:https://fluentassertions.com/objectgraphs/

    同样经过多年的经验,我仍然不推荐覆盖路线,我什至认为这是一种不好的做法。如果您不小心,可能会在使用某些集合(如字典)时引入性能问题。此外,当你有一个真正的商业案例来重载这些方法时,你会遇到麻烦,因为你已经有这个测试代码了。生产代码和测试代码应该分开,测试代码不应该依赖实现细节或黑客来实现他们的目标,这使得他们难以维护和理解。

    【讨论】:

    • 是的,您可以执行如下断言:actual.ShouldDeepEqual(expected); 其中实际和预期是您要比较的两个对象。
    【解决方案4】:

    ExpectedObjects 将帮助您按属性值比较相等性。它支持:

    1. 简单对象:expected.ToExpectedObject().ShouldEqual(actual);
    2. 集合:expected.ToExpectedObject().ShouldEqual(actual);
    3. 复合对象:expected.ToExpectedObject().ShouldEqual(actual);
    4. 部分比较:预期对象需要设计匿名类型,并使用 expected.ToExpectedObject().ShouldMatch(actual)

    我喜欢 ExpectedObjects,因为我只需要调用 2 个 API 来断言比较对象是否相等:

    1. ShouldEqual()
    2. ShouldMatch() 用于部分比较

    【讨论】:

    • 问题是,如果不相等怎么办,你必须找出问题所在,有什么不同。使用子关系或大量属性可能会花费大量时间。如果有人能解决这个问题,那就太好了。
    【解决方案5】:

    我想要一个不需要添加依赖项的解决方案,使用 VS 单元测试,比较两个对象的字段值,并告诉我所有不相等的字段。这就是我想出的。请注意,它也可以扩展为使用属性值。

    在我的例子中,这适用于比较某些文件解析逻辑的结果,以确保两个技术上“不同”的条目具有相同值的字段。

        public class AssertHelper
        {
            public static void HasEqualFieldValues<T>(T expected, T actual)
            {
                var failures = new List<string>();
                var fields = typeof(T).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
                foreach(var field in fields)
                {
                    var v1 = field.GetValue(expected);
                    var v2 = field.GetValue(actual);
                    if (v1 == null && v2 == null) continue;
                    if(!v1.Equals(v2)) failures.Add(string.Format("{0}: Expected:<{1}> Actual:<{2}>", field.Name, v1, v2));
                }
                if (failures.Any())
                    Assert.Fail("AssertHelper.HasEqualFieldValues failed. " + Environment.NewLine+ string.Join(Environment.NewLine, failures));
            }
        }
    
        [TestClass]
        public class AssertHelperTests
        {
            [TestMethod]     
            [ExpectedException(typeof(AssertFailedException))]           
            public void ShouldFailForDifferentClasses()
            {            
                var actual = new NewPaymentEntry() { acct = "1" };
                var expected = new NewPaymentEntry() { acct = "2" };
                AssertHelper.HasEqualFieldValues(expected, actual);
            }
        }
    

    【讨论】:

    • Nitpick:使用failures.Count &gt; 0 而不是failures.Any() 可以避免对System.Collections 的依赖。
    • 为了测试属性,我不得不将“typeof(T).GetFields(”改为“typeof(T).GetProperties(”。
    • if(!v1.Equals(v2)) 检查 v1 是否为 null,可能为 NullReferenceException。
    • 这不适用于内容相同但引用不同的引用类型的成员。
    【解决方案6】:

    对于微不足道的对象,例如域对象或 DTO 或实体,您可以简单地将两个实例序列化为一个字符串并比较该字符串:

    var object1Json = JsonConvert.SerializeObject(object1);
    var object2Json = JsonConvert.SerializeObject(object2);
    
    Assert.AreEqual(object1Json, object2Json);
    

    这当然有各种注意事项,因此请评估您的类,JSON 是否包含应比较的预期值。

    例如,如果您的类包含非托管资源或其他不可序列化的属性,则无法正确比较它们。默认情况下它也只序列化公共属性。

    【讨论】:

    • 为我工作。谢谢。
    【解决方案7】:

    由于在这个问题上还没有提到,所以还有几个其他被广泛采用的库可以帮助解决这个问题:

    1. Fluent Assertions - 见object graph comparison

      actual.Should().BeEquivalentTo(expected);

    2. Semantic Comparison

      Likeness&lt;MyModel, MyModel&gt;(actual).ShouldEqual(expected);

    我个人更喜欢 Fluent Assertions,因为它在成员排除等方面提供了更大的灵活性,并且它支持开箱即用的嵌套对象的比较。

    希望这会有所帮助!

    【讨论】:

      【解决方案8】:

      如果您只想比较复杂类型对象的属性, 遍历对象属性将给出所有属性。 你可以试试下面的代码。

      //Assert
      foreach(PropertyInfo property in Object1.GetType().GetProperties())
      {
          Assert.AreEqual(property.GetValue(Object1), property.GetValue(Object2));
      }
      

      【讨论】:

        最近更新 更多