如何在IEqualityComparer上实现单元测试?

时间:2017-07-06 09:44:28

标签: c# unit-testing nunit

我有一个实现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")));

这无法检测到添加了新属性,并且此属性在两个对象中不同。

有没有办法做到这一点?

如果可能,我们是否可以向属性添加属性以说明此属性与比较无关?

3 个答案:

答案 0 :(得分: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 - 方法。

EDIT2:正如评论中所提到的,在Equals甚至GetHashCode内进行反思是一个坏主意,因为它通常很慢并且通常可以避免。如果你知道在编译时要检查eqality的属性,你应该明确地将它们包含在这两种方法中,因为这样做会让你更安全。如果你发现自己真的需要这个,因为你需要很多属性,你可能会遇到一些基本问题,因为你的课程做得太多了。

答案 1 :(得分: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();

答案 2 :(得分: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);
        }
    }
}