Test 中驗證 Object 是否相同的方法

最近花了不少時間在重構先前專案中的 Unit Test 與 Integration Test,其中對於 reference type 的物件比對有幾種不同的寫法

當然我個人大多配合團隊規範,不會堅持某些做法,就是邊寫邊看邊學,如果有機會看到更好的方式就偷師幾招,今天就筆記一下個人在進行測試時驗證 期望物件 是否與 實際物件 相同的幾個做法

基本環境說明

  1. macOS Mojave 10.14.5
  2. .NET Core 2.2.301
  3. NuGet package

    • ExpectedObjects 2.3.4
    • FluentAssertions 5.7.0
  4. 測試用資料

    public class TestObject
    {
        public int Id { get; set; }
    
        public string Name { get; set;}
    
        public DateTime Birthday { get; set;}
    }
    

1. 覆寫 Equals

不建議只為了 測試 而覆寫 Object.Equals,這是本末倒置:不該為了測試目的而修改了 prodcution code 的行為,以下只是學術研究

  1. 繼承並實作 IEquatable<T>

    public class TestObject : IEquatable<TestObject>
    
  2. 實作 Equals

    public bool Equals(TestObject other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
    
       return this.Id == other.Id && this.Birthday == other.Birthday && this.Name == other.Name;
    }
    
  3. 覆寫 Object.EqualsGetHashCode

    測試 這個目的,這個步驟可以忽略,不會影響實際效果

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
    
        return obj.GetType() == this.GetType() && Equals((TestObject) obj);
    }
    
    public override int GetHashCode()
    {
        unchecked
        {
            return this.Id * 1983 * this.Birthday.GetHashCode() * 7 * this.Name.GetHashCode() * 29;
        }
    }
    
  4. 實際使用

    // Arrange
    var expected = new TestObject
    {
        Id = 1,
        Name = "Test",
        Birthday = new DateTime(1983, 7, 29)
    };
    
    
    // Act
    var actual = new TestObject
    {
        Id = 1,
        Name = "Test",
        Birthday = new DateTime(1983, 7, 29)
    };
    
    // Assert
    Assert.AreEqual(expected, actual);
    

2. 自訂比對

  1. 建立比對方法

    public static class CompareExtension
    {
        public static string ToStringNullSafe(this object obj)
        {
            return obj != null ? obj.ToString() : String.Empty;
        }
        public static bool Compare<T>(T actual, T expected, params string[] ignore)
        {
            var actualProps = actual.GetType().GetProperties();
            var expectedProps = expected.GetType().GetProperties();
            var count = actualProps.Length;
            for (var i = 0; i < count; i++)
            {
                var actualValue = actualProps[i].GetValue(actual, null).ToStringNullSafe();
                var expectedValue = expectedProps[i].GetValue(expected, null).ToStringNullSafe();
                if (actualValue != expectedValue && ignore.All(x => x != actualProps[i].Name))
                {
                    return false;
                }
            }
            return true;
        }
    }
    
  2. 實際使用

    // Arrange
    var expected = new TestObject
    {
        Id = 1,
        Name = "Test",
        Birthday = new DateTime(1983, 7, 29)
    };
    
    
    // Act
    var actual = new TestObject
    {
        Id = 1,
        Name = "Test",
        Birthday = new DateTime(1983, 7, 29)
    };
    
    // Assert
    Assert.IsTrue( CompareExtension.Compare(actual,expected));
    

3. 使用第三方套件

這是普遍推薦的作法,差異在於 assert fail 時會丟出詳細錯誤細節,對於 debug 相對友善

  • ExpectedObject

    // Arrange
    var expected = new TestObject
    {
        Id = 1,
        Name = "Test",
        Birthday = new DateTime(1983, 7, 29)
    };
    
    
    // Act
    var actual = new TestObject
    {
        Id = 1,
        Name = "Test",
        Birthday = new DateTime(1983, 7, 29)
    };
    
    // Assert
    expected.ToExpectedObject().ShouldEqual(actual);
    
  • FluentAssertions

    // Arrange
    var expected = new TestObject
    {
        Id = 1,
        Name = "Test",
        Birthday = new DateTime(1983, 7, 29)
    };
    
    
    // Act
    var actual = new TestObject
    {
        Id = 1,
        Name = "Test",
        Birthday = new DateTime(1983, 7, 29)
    };
    
    // Assert
    actual.Should().BeEquivalentTo(expected);
    

心得

以下是實際出現 assert fail 的訊息截圖

  1. 實作 IEquatable<T>

    _output_3objectequal

  2. 自訂比對

    _output_4customcompare

  3. ExpectedObject

    _output_1expectedobject

  4. FluentAssertions

    _output_2fluentassertions

看完 assert fail 的訊息後,我立馬就放棄了 實作 IEquatable<T>自訂比對,提示訊息很不友善,對於快速定位問題源頭沒有幫助

至於兩個 NuGet package : ExpectedObjectFluentAssertions,我個人是比較常用 FluentAssertions,因為 FluentAssertions 還可以用來將 assert 做語意化的呈現,而 ExpectedObject 就是只用來比較 object 是否相同,功能相對比較侷限

參考資訊

  1. Asserting Equality in your C# unit tests
  2. How to Compare Object Instances in your Unit Tests Quickly and Easily