[Unit Test Tricks] 如何驗證兩個自訂型別物件集合相等

[Unit Testing]如何驗證兩個自訂型別物件集合相等

前言

雖然標題上掛著測試,但其實比較兩個自訂型別物件是否相等,是屬於 C# 的基本觀念,然而卻是在寫測試程式時,困擾著很多朋友的一道門檻。

這篇文章只會簡單的帶過 C# 的基本觀念,重點會放在測試程式中,怎麼驗證兩個集合中物件的值是否相等。

 

AreSame() VS AreEqual()

在 MSTest Framework 中,Assert 中的 AreEqual()AreSame() 是不一樣的。

AreSame() 指兩個物件是否為相同的物件,也就是變數的參考位址是否一致。如果是 value type ,那通常就是不一致的,因為 value type 是被存放在 stack 的記憶體中,如下面的測試結果會是失敗的。


        [TestMethod]
        public void Test_AreSame()
        {
            var expected = 1;
            var actual = 1;

            // 比較 expected 與 actual 必須是同一個位址參考的物件
            Assert.AreSame(expected, actual);
        }

同樣的例子,使用 AreEqual() 則會通過測試,原因是 value type 通常都會覆寫 Equals() 的相關方法。


        [TestMethod]
        public void Test_AreEqual()
        {
            var expected = 1;
            var actual = 1;
            
            Assert.AreEqual(expected, actual);
        }

簡單地說就是:AreSame() 一定要是同一塊記憶體位址,也就是要同一個物件,才會是 true 。 AreEqual() 則看比較的物件型別如何定義 Equals() ,若無定義,則是使用繼承自 Object 型別上的 Equals() 方法,也就是比較位址。因此,如果是自己定義的 class ,沒有覆寫 Equals() ,那麼預設就是比記憶體位址是否相同,這時 AreSame()AreEqual() 的結果會是一樣的。

 

比較自訂 Customer 型別的物件是否相等

有了上面對 AreSame()AreEqual() 的了解,這邊第一個範例先來說明,當自訂一個 Customer 型別時,在測試中該如何比較兩個 Customer instance 是否相等,測試程式如下:


    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void 驗證Cusotmer是否相同_Same()
        {            
            var expected = new Customer { Id = 1, Age = 10, Name = "Joey", Phone = "1999" };
            var actual = new Customer { Id = 1, Age = 10, Name = "Joey", Phone = "1999" };
 
            // 比較 expected 與 actual 必須是同一個位址參考的物件
            Assert.AreSame(expected, actual);
        }
 
        [TestMethod]
        public void 驗證Cusotmer是否相等_Equal()
        {            
            var expected = new Customer { Id = 1, Age = 10, Name = "Joey", Phone = "1999" };
            var actual = new Customer { Id = 1, Age = 12, Name = "Joey", Phone = "2000" };
 
            // 即使 Age 與 Phone 值不一樣,但只要 Customer.Equals 回傳是 true, Assert.AreEqual() 就會是 true
            Assert.AreEqual(expected, actual);
        }
    }
 
    public class Customer
    {
        /// <summary>
        /// 若 Id 與 Name 值一樣就代表相等
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public override bool Equals(object obj)
        {
            // 正常來說覆寫 Equals() 應該也要覆寫 GetHashCode()
            // 一般的情況,如果只是要比較兩個 Customer 物件是否相同,不建議覆寫 Equals 與 GetHashCode(),而是使用 IEqualityComparer<T> 來定義怎麼比較 T 是否相等
            var input = obj as Customer;
            if (input == null)
            {
                return false;
            }
            else
            {
                var result = this.Id == input.Id
                    && this.Name == input.Name;
                return result;
            }
        }
 
        public int Id { get; set; }
 
        public int Age { get; set; }
 
        public string Name { get; set; }
 
        public string Phone { get; set; }
    }

使用 AreSame() 驗證兩個 Customer instance 的測試方法,肯定會失敗,因為兩個物件本來就不相同。但是如果是要驗證兩個物件是不是相等,就可以依據需求覆寫 Customer 的 Equals() 。以上面的例子來說,雖然 Customer 上有 Id, Age, Name, Phone 四個 property ,但只要 Id 與 Name 的值相同,我們就視為這兩個 Customer instance 相等,那就可以如上面的範例一樣,直接覆寫 Equals() 方法,並比較 Id 與 Name 有沒相等即可。

然而,單一物件可以直接覆寫 Equals() 來定義何謂相等,但如果是兩個 Customer 的集合呢?

 

使用 CollectionAssert 比較兩個 Customer 集合是否相等

要怎麼比較兩個 Customer 集合是否相等呢?這邊的例子我使用的集合是 List<T> , 這也就代表它也是 ICollection<T>, 也是 IEnumerable<T> 。範例如下:


    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void 驗證Cusotmer集合是否相等_Assert_AreEqual()
        {
            var expected = GetCustomers();
            var actual = GetCustomers();

            // 因為兩個 List<Customer> 的 instance 不是同一個物件,所以驗證失敗
            Assert.AreEqual(expected, actual);
        }

        [TestMethod]
        public void 驗證Cusotmer集合是否相等_CollectionAssert_AreEqual()
        {
            var expected = GetCustomers();
            var actual = GetCustomers();

            CollectionAssert.AreEqual(expected, actual);
            
        }

        private List<Customer> GetCustomers()
        {
            return new List<Customer>
            {
                new Customer{Id=1, Age= 10, Name = "Joey", Phone="1999"},
                new Customer{Id=2, Age= 20, Name = "Kevin", Phone="2999"},
                new Customer{Id=3, Age= 30, Name = "Demo", Phone="3999"},
            };
        }
    }

    public class Customer
    {
        public int Id { get; set; }

        public int Age { get; set; }

        public string Name { get; set; }

        public string Phone { get; set; }

        public override bool Equals(object obj)
        {
            // 正常來說覆寫 Equals() 應該也要覆寫 GetHashCode()
            // 一般的情況,如果只是要比較兩個 Customer 物件是否相同,不建議覆寫 Equals 與 GetHashCode(),而是使用 IEqualityComparer<T> 來定義怎麼比較 T 是否相等
            var input = obj as Customer;
            if (input == null)
            {
                return false;
            }
            else
            {
                var result = this.Id == input.Id
                    && this.Name == input.Name;
                return result;
            }
        }
    }

如上一段所提,使用 AreEqual() 直接拿來比較兩個 List<Customer> 結果一定會是 false ,因為兩個 List<Customer> 是不同的 instance ,位址當然就不一樣。

但如果使用 CollectionAssert.AreEqual() 來比較兩個 List<Customer> 時,則測試的結果會是通過,因為讀者可以想像 CollectionAssert.AreEqual() 背後運作,就是將兩個集合攤開來跑,每一個 element 都必須相等,而這個相等會呼叫該 element 的 Equals() 方法來比較。

image

請留意 MSDN 繁體中文上備註的解釋,有一段是翻譯錯誤的。英文原文:「Elements are equal if their values are equal, not if they refer to the same object.」,中文的意思應該是:「當結果為相等時,代表每個項目的值都相等,而不是指每個比較的項目都必須是相同物件。」,目前 MSDN 上錯誤的中文翻譯是:「如果項目的值相等,它們就相等,但是如果它們參考相同的物件,則不相等。」

然而, Customer 物件可能是在 production code 中定義的,我們無法直接覆寫其 Equals() ,又或者是在不同情境中或是在測試案例中, Customer 物件相等的定義可能不同,此時一般來說應該使用 IEqualityComparer<Customer>  來定義特定的相等條件。因為是使用介面,所以當需要不同相等的定義時,只需要傳入不同的實作即可。

然而 CollectionAssert 的 AreEqual() 多載中,傳入的參數並非 IEqualityComparer ,而是 IComparer ,筆者自己從 MSDN 上說明的推測,是因為 CollectionAssert 還要比較順序, IComparer 除了比較相等以外,還能進行排序。另外一個比較不方便的地方是,傳入的 IComparer 並非泛型的 ICollection<T> ,且尷尬的是 ICollection<T> 又剛好沒有繼承自 ICollection,想來應該是因為整個 CollectionAssert 是基於 ICollection 而非 ICollection<T> 的原因。

沒關係,來看一下該怎麼使用 CollectionAssert.AreEqual(ICollection expected, ICollection actual, IComparer comparer) 的方法,測試程式碼如下所示:


    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void 驗證Cusotmer集合是否相等_CollectionAssert_AreEqual()
        {
            var expected = GetCustomers();
            var actual = GetCustomers();

            //CollectionAssert.AreEqual(expected, actual);
            CollectionAssert.AreEqual(expected, actual, new MyCustomerComparer());
        }

        [TestMethod]
        public void 驗證Cusotmer集合是否相等_CollectionAssert_AreEqual_只有Id跟Name相等()
        {
            var expected = new List<Customer>
            {
                new Customer{Id=1, Age= 10, Name = "Joey", Phone="1999"},
                new Customer{Id=2, Age= 20, Name = "Kevin", Phone="2999"},
                new Customer{Id=3, Age= 30, Name = "Demo", Phone="3999"},
            };

            var actual = new List<Customer>
            {
                new Customer{Id=1, Age= 40, Name = "Joey", Phone="1000"},
                new Customer{Id=2, Age= 50, Name = "Kevin", Phone="2000"},
                new Customer{Id=3, Age= 60, Name = "Demo", Phone="3000"},
            };

            //CollectionAssert.AreEqual(expected, actual);
            CollectionAssert.AreEqual(expected, actual, new MyCustomerComparer());
        }

        private List<Customer> GetCustomers()
        {
            return new List<Customer>
            {
                new Customer{Id=1, Age= 10, Name = "Joey", Phone="1999"},
                new Customer{Id=2, Age= 20, Name = "Kevin", Phone="2999"},
                new Customer{Id=3, Age= 30, Name = "Demo", Phone="3999"},
            };
        }
    }

    public class Customer
    {
        public int Id { get; set; }

        public int Age { get; set; }

        public string Name { get; set; }

        public string Phone { get; set; }
    }

    internal class MyCustomerComparer : IComparer, IComparer<Customer>
    {
        public int Compare(object x, object y)
        {
            if (x is Customer && y is Customer)
            {
                return this.Compare((Customer)x, (Customer)y);
            }
            else
            {
                throw new ArgumentException("傳入參數非Customer型別");
            }
        }

        public int Compare(Customer x, Customer y)
        {
            if (x.Id.CompareTo(y.Id) != 0)
            {
                return x.Id.CompareTo(y.Id);
            }
            else if (x.Name.CompareTo(y.Name) != 0)
            {
                return x.Name.CompareTo(y.Name);
            }
            else
            {
                return 0;
            }
        }
    }

首先留意到 Customer 類別已經沒有覆寫 Equals() 了,這個例子是透過 IComparer 來比較兩個 ICollection<Customer> ,也由於 IComparer<T> 並沒有繼承自 IComparer ,所以這邊定義了一個 MyCustomerComparer 同時實作 IComparer 與 IComparer<T> ,讓非泛型介面的實作方法,去呼叫泛型介面的方法即可。在這個例子中,定義了兩個 Customer 物件只要 Id 與 Name 相等,就代表這兩個 Customer 物件相等。因此,這兩個測試案例都會通過。

使用 IComparer 來比較相等的好處是,可以獨立於原本的 Customer class 定義之外,且允許多種不同的比較方式。

但是,可以看到使用 CollectionAssert 似乎還是有點麻煩,尤其是卡東卡西的 ICollection 。接下來介紹另一種比較方式,是透過 LINQ 的 SequenceEqual() 來比較兩個集合是否相等。

 

使用 SequenceEqual() 比較兩個 Customer 集合是否相等

雖然以嚴謹的單元測試定義來說,在測試程式中,要盡量避免相依於其他的 API ,因為當測試失敗或發生錯誤時,才能更精準、明確地定義就是這個測試案例發生問題,而不是被其他因素影響了。但 LINQ 的方法基本上不算外部的 API ,把 LINQ 的方法當作不會有錯也還算是一個合理的假設。因此,接下來的例子,就是透過 IEnumerable<T> 的擴充方法 SequenceEqual() 來比較兩個 IEnumerable<Customer> 是否相等。測試程式如下所示:


    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void 驗證Cusotmer集合是否相等_SequenceEqual()
        {
            var expected = GetCustomers();
            var actual = GetCustomers();

            //CollectionAssert.AreEqual(expected, actual, new MyCustomerComparer());
            Assert.IsTrue(expected.SequenceEqual(actual, new MyCustomerEqualityComparer()));
        }

        [TestMethod]
        public void 驗證Cusotmer集合是否相等_SequenceEqual_只有Id跟Name相等()
        {
            var expected = new List<Customer>
            {
                new Customer{Id=1, Age= 10, Name = "Joey", Phone="1999"},
                new Customer{Id=2, Age= 20, Name = "Kevin", Phone="2999"},
                new Customer{Id=3, Age= 30, Name = "Demo", Phone="3999"},
            };

            var actual = new List<Customer>
            {
                new Customer{Id=1, Age= 40, Name = "Joey", Phone="1000"},
                new Customer{Id=2, Age= 50, Name = "Kevin", Phone="2000"},
                new Customer{Id=3, Age= 60, Name = "Demo", Phone="3000"},
            };

            //CollectionAssert.AreEqual(expected, actual, new MyCustomerComparer());
            Assert.IsTrue(expected.SequenceEqual(actual, new MyCustomerEqualityComparer()));
        }

        private List<Customer> GetCustomers()
        {
            return new List<Customer>
            {
                new Customer{Id=1, Age= 10, Name = "Joey", Phone="1999"},
                new Customer{Id=2, Age= 20, Name = "Kevin", Phone="2999"},
                new Customer{Id=3, Age= 30, Name = "Demo", Phone="3999"},
            };
        }
    }

    public class MyCustomerEqualityComparer : EqualityComparer<Customer>
    {
        public override bool Equals(Customer x, Customer y)
        {
            return x.Id == y.Id
                && x.Name == y.Name;
        }

        public override int GetHashCode(Customer obj)
        {
            // 一樣,這邊只focus在 Equals, 沒有要給 Dictionary 拿來當 Key 使用
            return 0;
        }
    }

IEnumerable<T>.SequenceEqual() 若沒傳入 IEqualityComparer<T> 參數,則預設也是使用 T 的 Equals() 方法來比較是否相等。因此這裡自訂一個 MyCustomerEqualityComparer 的 class 繼承自 EqualityComparer<Customer> ,自然也就實作了 IEqualityComparer<Customer> 。一樣覆寫 Equals() ,要注意一下這個 Equals 並不是 Object 的 Equals() 方法,只要簡單的定義當這兩個 Customer 物件的 Id 與 Name 值相等,就代表相等。

在原本使用 CollectionAssert.AreEqual() 的部分,就可以改用 Assert.IsTrue(expected.SequenceEqual(actual)) 來比較了。相較於 CollectionAssert 的 AreEqual() ,我個人比較喜歡用 SequenceEqual() 來比較集合。

但每次要比較不同 property 的值,就需要自訂一個 EqualityComparer 的類別,感覺好麻煩,有沒有更簡單的方式?有,接下來要介紹兩個偷吃步的方式,當只是在測試程式中要驗證兩個集合中物件的某些值是否相等時,可以不需定義 class 的方式。

 

使用匿名型別來比較兩個 Customer 集合是否相等

是的,你沒看錯,用匿名型別來比。因為匿名型別的 Equals()GetHashCode() 是依據匿名型別上每個 property 的 Equals()GetHashCode() 來比較,只有完全相等才叫相等。(請參考:匿名類型 (C# 程式設計手冊))來看一下怎麼用匿名型別來改寫剛剛的測試程式,如下所示:


    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void 驗證Cusotmer集合是否相等_SequenceEquals_使用匿名型別()
        {
            var expected = GetCustomers();
            var actual = GetCustomers();

            // 使用匿名型別取得放置 Id 與 Name 的容器,就可以針對 Id 與 Name 進行驗證是否相等
            var expectedValues = expected.Select(x => new { Id = x.Id, Name = x.Name });
            var actualValues = actual.Select(x => new { Id = x.Id, Name = x.Name });

            //Assert.IsTrue(expected.SequenceEqual(actual, new MyCustomerEqualityComparer()));
            Assert.IsTrue(expectedValues.SequenceEqual(actualValues));
        }

        [TestMethod]
        public void 驗證Cusotmer集合是否相等_SequenceEquals_只有Id跟Name相等_使用匿名型別()
        {
            var expected = new List<Customer>
            {
                new Customer{Id=1, Age= 10, Name = "Joey", Phone="1999"},
                new Customer{Id=2, Age= 20, Name = "Kevin", Phone="2999"},
                new Customer{Id=3, Age= 30, Name = "Demo", Phone="3999"},
            };

            var actual = new List<Customer>
            {
                new Customer{Id=1, Age= 40, Name = "Joey", Phone="1000"},
                new Customer{Id=2, Age= 50, Name = "Kevin", Phone="2000"},
                new Customer{Id=3, Age= 60, Name = "Demo", Phone="3000"},
            };

            // 使用匿名型別取得放置 Id 與 Name 的容器,就可以針對 Id 與 Name 進行驗證是否相等
            var expectedValues = expected.Select(x => new { Id = x.Id, Name = x.Name });
            var actualValues = actual.Select(x => new { Id = x.Id, Name = x.Name });

            //Assert.IsTrue(expected.SequenceEqual(actual, new MyCustomerEqualityComparer()));
            Assert.IsTrue(expectedValues.SequenceEqual(actualValues));
        }

        private List<Customer> GetCustomers()
        {
            return new List<Customer>
            {
                new Customer{Id=1, Age= 10, Name = "Joey", Phone="1999"},
                new Customer{Id=2, Age= 20, Name = "Kevin", Phone="2999"},
                new Customer{Id=3, Age= 30, Name = "Demo", Phone="3999"},
            };
        }
    }

透過匿名型別相等的定義,這樣測試程式寫起來是不是更加輕鬆愉快了呢?

這個方式仍有些討人厭的地方,就是那一段透過 Select() 來產生匿名型別的部分,要自己定義匿名型別的 property ,感覺起來也好囉唆,有沒有更偷吃步的方式?有的,要偷吃步到一個極限,我們還可以用 Tuple 來做。

對 Tuple 還不是很瞭解的讀者,可以參考:[C#]Tuple 簡介

 

使用 Tuple 來比較兩個 Customer 集合是否相等

會想使用 Tuple 來簡化比較相等的動作,當然是因為 Tuple 的相等比較跟匿名型別一樣,會使用各個型別參數物件的 Equals() 來比較。但在測試程式中,用起來會比匿名型別更簡單一些,測試程式如下:


        [TestMethod]
        public void 驗證Cusotmer集合是否相等_SequenceEquals_使用Tuple()
        {
            var expected = GetCustomers();
            var actual = GetCustomers();

            //var expectedValues = expected.Select(x => new { Id = x.Id, Name = x.Name });
            //var actualValues = actual.Select(x => new { Id = x.Id, Name = x.Name });
            var expectedTuple = expected.Select(x => Tuple.Create(x.Id, x.Name));
            var actualTuple = actual.Select(x => Tuple.Create(x.Id, x.Name));

            //Assert.IsTrue(expectedValues.SequenceEqual(actualValues));
            Assert.IsTrue(expectedTuple.SequenceEqual(actualTuple));
        }

        [TestMethod]
        public void 驗證Cusotmer集合是否相等_SequenceEquals_只有Id跟Name相等_使用Tuple()
        {
            var expected = new List<Customer>
            {
                new Customer{Id=1, Age= 10, Name = "Joey", Phone="1999"},
                new Customer{Id=2, Age= 20, Name = "Kevin", Phone="2999"},
                new Customer{Id=3, Age= 30, Name = "Demo", Phone="3999"},
            };

            var actual = new List<Customer>
            {
                new Customer{Id=1, Age= 40, Name = "Joey", Phone="1000"},
                new Customer{Id=2, Age= 50, Name = "Kevin", Phone="2000"},
                new Customer{Id=3, Age= 60, Name = "Demo", Phone="3000"},
            };

            //var expectedValues = expected.Select(x => new { Id = x.Id, Name = x.Name });
            //var actualValues = actual.Select(x => new { Id = x.Id, Name = x.Name });
            var expectedTuple = expected.Select(x => Tuple.Create(x.Id, x.Name));
            var actualTuple = actual.Select(x => Tuple.Create(x.Id, x.Name));

            //Assert.IsTrue(expectedValues.SequenceEqual(actualValues));
            Assert.IsTrue(expectedTuple.SequenceEqual(actualTuple));
        }

不管是匿名型別的方式或是 Tuple 的方式,這邊的例子剛好是使用 Lambda 的方式來舉例,這不代表匿名型別或 Tuple 的比較方式,就不能重用。別忘了,這都只是 Select() 參數傳入的方式罷了。要重用相等的定義條件時,就自訂一個參數能滿足 Func<TSource, TResult> 來重用即可。這邊舉了兩個方式當例子,一個是 Func ,一個則是單純滿足 delegate 簽章的 private function ,如下所示:


        [TestMethod]
        public void 驗證Cusotmer集合是否相等_SequenceEquals_使用Tuple()
        {
            var expected = GetCustomers();
            var actual = GetCustomers();

            //var expectedValues = expected.Select(x => new { Id = x.Id, Name = x.Name });
            //var actualValues = actual.Select(x => new { Id = x.Id, Name = x.Name });
            var expectedTuple = expected.Select(x => Tuple.Create(x.Id, x.Name));
            var actualTuple = actual.Select(x => Tuple.Create(x.Id, x.Name));

            //Assert.IsTrue(expectedValues.SequenceEqual(actualValues));
            Assert.IsTrue(expectedTuple.SequenceEqual(actualTuple));
        }

        [TestMethod]
        public void 驗證Cusotmer集合是否相等_SequenceEquals_只有Id跟Name相等_使用Tuple()
        {
            var expected = new List<Customer>
            {
                new Customer{Id=1, Age= 10, Name = "Joey", Phone="1999"},
                new Customer{Id=2, Age= 20, Name = "Kevin", Phone="2999"},
                new Customer{Id=3, Age= 30, Name = "Demo", Phone="3999"},
            };

            var actual = new List<Customer>
            {
                new Customer{Id=1, Age= 40, Name = "Joey", Phone="1000"},
                new Customer{Id=2, Age= 50, Name = "Kevin", Phone="2000"},
                new Customer{Id=3, Age= 60, Name = "Demo", Phone="3000"},
            };

            //var expectedTuple = expected.Select(x => Tuple.Create(x.Id, x.Name));
            //var actualTuple = actual.Select(x => Tuple.Create(x.Id, x.Name));
            var expectedTuple = expected.Select(x => this.ComparerIdAndName(x));
            var actualTuple = actual.Select(x => this.ComparerIdAndName(x));
            Assert.IsTrue(expectedTuple.SequenceEqual(actualTuple));

            var expectedTupleByFunc = expected.Select(funcComparerIdAndName);
            var actualTupleByFunc = actual.Select(funcComparerIdAndName);
            Assert.IsTrue(expectedTupleByFunc.SequenceEqual(actualTupleByFunc));
        }

        private Func<Customer, Tuple<int, string>> funcComparerIdAndName = customer => { return Tuple.Create(customer.Id, customer.Name); };

        private Tuple<int, string> ComparerIdAndName(Customer customer)
        {
            return Tuple.Create(customer.Id, customer.Name);
        }

對 Func<T> 不熟悉的讀者,請參考:[.NET]快快樂樂學LINQ系列前哨戰-Func, Action, Predicate ,對 Select() 不熟悉的讀者,請參考:[.NET]快快樂樂學LINQ系列-Select() 簡介

 

結論

歸納一下幾個重點:

  1. AreSame() 指兩個變數必須是同一個物件,也就是相同,也就是指到同一塊記憶體。AreEqual() 則是會使用變數型別的 Equals() 來進行比較相等,背後運作有可能是先使用 Object.Equals() 這個靜態方法來比較 expected 與 actual 。
  2. 針對集合,可以使用 CollectionAssert.AreEqual() 搭配 IComparer ,好處是允許不同的相等定義,且無須覆寫原本自訂型別的 Equals() 。壞處是並不直接支援泛型,而且多個不同的相等定義,需要撰寫多組 IComparer 的類別。
  3. 針對集合,可以使用 LINQ 的 SequenceEqual() 搭配 IEqualityComparer 來比較兩個集合是否相等,有 CollectionAssert.AreEqual() 的好處,又可以享有泛型的支援。壞處是針對多個不同的相等定義,仍要撰寫多組 EqualityComparer 的類別。
  4. 使用匿名型別,搭配 LINQ 的 Select() 將 expected 與 actual 的集合 projection 成只需要比較特定值的匿名型別集合。好處是因為匿名型別的相等比較子會針對每一個 property 來比較是否相等,因此就不需要再額外撰寫多組相等比較子。壞處是匿名型別的 property 名稱還是要自己定義,但其實 property 名稱在測試中根本沒有意義。
  5. 使用 Tuple ,搭配 LINQ 的 Select() 將 expected 與 actual 的集合 projection 成只需要比較特定值的 Tuple 集合,好處是 Tuple 的相等比較子與匿名型別的方式一樣,而且連 property 名稱都可以省下來,因為 Tuple 的 property 名稱就是 Item1, Item2, ..., ItemN。壞處是當要比較的 property 超過 7 個以上時,就會長得比較複雜,需要用鏈狀的 Tuple 來串接。

雖然介紹了一些偷吃步的方式,但在實務測試上,這些都是可以簡化測試程式撰寫的眉角,希望對大家有所幫助,雖然用到了不少 C# 基礎知識。對匿名型別、Tuple、LINQ、Func<T> 跟 Lambda 還不熟的朋友,建議趁機把這些知識補起來,在實務設計上,一定會發揮很大的效用。

 

Reference

  1. Tuple<T1, T2>.Equals 方法
  2. 匿名類型 (C# 程式設計手冊)
  3. Assert.AreSame 方法
  4. Assert.AreEqual 方法 (Object, Object)
  5. IEqualityComparer<T> 介面
  6. IComparer<T> 介面
  7. IComparer 介面
  8. CollectionAssert.AreEqual 方法 (ICollection, ICollection, IComparer)
  9. Enumerable.SequenceEqual<TSource> 方法 (IEnumerable<TSource>, IEnumerable<TSource>)
  10. [C#]Tuple 簡介
  11. [.NET]快快樂樂學LINQ系列前哨戰-var與匿名型別
  12. [.NET]快快樂樂學LINQ系列前哨戰-Func, Action, Predicate
  13. [.NET]快快樂樂學LINQ系列-Select() 簡介

對敏捷開發有興趣的朋友,可以參考我的粉絲專頁:91敏捷開發之路

若需要聯絡我,可以透過粉絲專頁私訊或是側欄的關於我。