2017-07-06 23 views
4

我有一個類和用於此類,它實現一個IEqualityComparer比較器:如何在IEqualityComparer上實現單元測試?

class Foo 
{ 
    public int Int { get; set; } 
    public string Str { get; set; } 

    public Foo(int i, string s) 
    { 
     Int = i; 
     Str = s; 
    } 

    private sealed class FooEqualityComparer : IEqualityComparer<Foo> 
    { 
     public bool Equals(Foo x, Foo y) 
     { 
      if (ReferenceEquals(x, y)) return true; 
      if (ReferenceEquals(x, null)) return false; 
      if (ReferenceEquals(y, null)) return false; 
      if (x.GetType() != y.GetType()) return false; 
      return x.Int == y.Int && string.Equals(x.Str, y.Str); 
     } 

     public int GetHashCode(Foo obj) 
     { 
      unchecked 
      { 
       return (obj.Int * 397)^(obj.Str != null ? obj.Str.GetHashCode() : 0); 
      } 
     } 
    } 

    public static IEqualityComparer<Foo> Comparer { get; } = new FooEqualityComparer(); 
} 

兩種方法EqualsGetHashCode經由比較器的一個實例是用於例如List.Except

我的問題是:如何在這個比較器上正確實現單元測試?我想檢測是否有人在不修改比較器的情況下在Foo中添加公共屬性,因爲在這種情況下,比較器將變爲無效。

如果我做這樣的事情:

Assert.That(new Foo(42, "answer"), Is.EqualTo(new Foo(42, "answer"))); 

這不能檢測到添加了新的屬性,這個屬性的兩個對象不同。

有沒有辦法做到這一點?

如果可能,我們可以添加一個屬性到一個屬性來說這個屬性在比較中是不相關的嗎?

+0

你不能指望你的比較器比較不知道它們存在的屬性。您必須更改比較器以檢查新屬性。你可以通過使用反射來實現這一點,但這會讓你的比較器瘋狂地變慢。 – HimBromBeere

+1

如果您打算將它用作哈希表中的鍵,那麼'Foo'應該是不可變的。在這種情況下,需要使用ctor的paramsters來設置新的屬性,並且任何這樣做的人都會看到您對Comparer的測試不再編譯。 (並希望更新比較器和測試)。 另一個簡單的方法,只需在Foo中添加一條評論,即比較者XXX需要更新。 – Magnus

+0

@Magnus這似乎很有趣,但如何做到這一點? – Boiethios

回答

1

您可以使用反射來獲取該類型的屬性,例如:

var knownPropNames = new string[] 
{ 
    "Int", 
    "Str", 
}; 
var props = typeof(Foo).GetProperties(BindingFlags.Public | BindingFlags.Instance); 
var unknownProps = props 
        .Where(x => !knownPropNames.Contains(x.Name)) 
        .Select(x => x.Name) 
        .ToArray(); 
// Use assertion instead of Console.WriteLine 
Console.WriteLine("Unknown props: {0}", string.Join("; ", unknownProps)); 

這樣,你就可以實現一個測試,如果添加任何屬性失敗。當然,你必須在開始時向數組添加新的屬性。從性能角度來看,使用反射是一項昂貴的操作,如果需要比較大量對象,我會建議在測試中使用它,而不是在比較器本身中使用它。

還請注意使用BindingFlags參數,以便您可以將屬性限制爲僅限於公共級別和實例級別的屬性。

另外,您可以定義用於標記不相關屬性的自定義屬性。例如:

[AttributeUsage(AttributeTargets.Property)] 
public class ComparerIgnoreAttribute : Attribute {} 

你可以把它應用到一個屬性:

[ComparerIgnore] 
public decimal Dec { get; set; } 

此外,你不得不延長該發現未知屬性代碼:

var unknownProps = props 
        .Where(x => !knownPropNames.Contains(x.Name) 
         && !x.GetCustomAttributes(typeof(ComparerIgnoreAttribute)).Any()) 
        .Select(x => x.Name) 
        .ToArray(); 
+0

我更喜歡你的解決方案,因爲它不會導致運行時開銷,並且只能在測試中放置。 – Boiethios

1

基本上,你應該看看Equals的所有房產。要篩選一些人出來的那些屬性使用屬性:在您的比較器檢查

class Foo 
{ 
    [MyAttribute] 
    public string IgnoredProperty { get; set; } 
    public string MyProperty { get; set; } 
} 

現在對於該特定屬性。隨後通過PropertyInfo.GetValue

class MyComparer : IEqualityComparer<Foo> 
{ 
    public bool Equals(Foo x, Foo y) 
    { 
     var properties = this.GetType().GetProperties() 
       .Where(x => "Attribute.IsDefined(x, typeof(MyAttribute)); 
     var equal = true; 
     foreach(var p in properties) 
      equal &= p.GetValue(x, null) == p.GetValue(y, null); 
     return equal; 
    } 
} 

包含在剩下的列表中的每個屬性比較然而,你應該有內GetHashCode一些很好的預檢查,以避免這種緩慢的方法不必要的調用。

編輯:正如你剛纔提到的ReSharper,我假設你提供了在運行時驗證的實際屬性,甚至R#不知道實現GetHashCode的好方法。您將需要一些屬性,這些屬性可以在您的類型上使用,並提供足夠的關於什麼可能被認爲是平等的想法。所有其他屬性,但應該只進入昂貴的Equals-方法。

編輯2:正如在Equals或甚至GetHashCode內做出反應的評論中提到的,這是一個壞主意,因爲它通常很慢並且經常可以避免。如果你知道編譯時要檢查的eqality的屬性,你應該首先將它們包含在這兩種方法中,因爲這樣做會給你提供更多的安全性。當你發現你真的需要這個,因爲你有很多屬性,你可能有一些基本的問題,因爲你的班級做得太多。

+0

我讓Resharper實現哈希計算。這應該是好的。 – Boiethios

+0

@Boiethios看我的編輯。 – HimBromBeere

+0

我會強烈避免在Equals或GetHashCode中使用反射。他們縮進快,而不是。 – Magnus

1

我猜你可以檢查比較器內的屬性數量。像這樣:

private sealed class FooEqualityComparer : IEqualityComparer<Foo> 
{ 
    private List<bool> comparisonResults = new List<bool>(); 
    private List<Func<Foo, Foo, bool>> conditions = new List<Func<Foo, Foo, bool>>{ 
     (x, y) => x.Int == y.Int, 
     (x, y) => string.Equals(x.Str, y.Str) 
    }; 
    private int propertiesCount = typeof(Foo) 
       .GetProperties(BindingFlags.Public | BindingFlags.Instance) 
       //.Where(someLogicToExclde(e.g attribute)) 
       .Count(); 

    public bool Equals(Foo x, Foo y) 
    { 
     if (ReferenceEquals(x, y)) return true; 
     if (ReferenceEquals(x, null)) return false; 
     if (ReferenceEquals(y, null)) return false; 
     if (x.GetType() != y.GetType()) return false; 
     //has new property which is not presented in the conditions list and not excluded 
     if (conditions.Count() != propertiesCount) return false;  

     foreach(var func in conditions) 
      if(!func(x, y)) return false;//returns false on first mismatch 

     return true;//only if all conditions are satisfied 
    } 

    public int GetHashCode(Foo obj) 
    { 
     unchecked 
     { 
      return (obj.Int * 397)^(obj.Str != null ? obj.Str.GetHashCode() : 0); 
     } 
    } 
} 
+0

這個解決方案看起來不錯,但是'Equals'方法會比較慢(我會在大量的對象上調用這個方法)。 – Boiethios

+0

我有一個想法如何加快它。一分鐘 –

+0

@Boiethios,看我的更新 –

相關問題