简体   繁体   English

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

[英]How to implement unit tests on IEqualityComparer?

I have a class and a comparer for this class that implements 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();
}

The two methods Equals and GetHashCode are used for example in List.Except via an instance of the comparer. EqualsGetHashCode这两个方法例如在List.Except通过比较器的实例使用。

My question is: how to implement properly unit tests on this comparer? 我的问题是:如何在这个比较器上实现正确的单元测试? I want to detect if someone adds a public property in Foo without modifying the comparer, because in this case the comparer becomes invalid. 我想检测是否有人在Foo添加公共属性而不修改比较器,因为在这种情况下,比较器变为无效。

If I do something like: 如果我这样做:

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

This cannot detect that a new property was added, and that this property differs in the two objects. 这无法检测到添加了新属性,并且此属性在两个对象中不同。

Is there any way to do this? 有没有办法做到这一点?

If it is possible, can we add an attribute to a property to say that this property is not relevant in the comparison? 如果可能,我们是否可以向属性添加属性以表明此属性与比较无关?

Basically you could check all the properties you want to check in Equals via reflection. 基本上,你可以检查所有要检查的属性Equals通过反射。 To filter some of them out use an attribute on those properties: 要过滤其中一些属性,请使用这些属性的属性:

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

Now in your comparer check for that specific attribute. 现在在比较器中检查该特定属性。 Afterwards compare every property that is contained in the remaining list via PropertyInfo.GetValue 然后通过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;
    }
}

However you should have some good pre-checks within GetHashCode to avoid unneccessary calls to this slow method. 但是,您应该在GetHashCode进行一些良好的预检查,以避免对此慢速方法进行不必要的调用。

EDIT: As you´ve mentioned ReSharper, I assume as you provide the actual properties to be validated at runtime even R# doesn´t know a good way to implement GetHashCode . 编辑:正如您提到的ReSharper,我假设您提供了在运行时验证的实际属性,即使R#不知道实现GetHashCode的好方法。 You will need some properties that will allways be available on your type and that provide a good enough idea of what might be considered equal. 您将需要一些属性,这些属性将始终可用于您的类型,并提供对可能被视为相等的内容的充分了解。 All theadditional properties however should only go into the expensive Equals -method. 然而,所有附加属性应该只进入昂贵的Equals -method。

EDIT2: As mentioned in the comments doing reflection within Equals or even GetHashCode is a bad idea as it´s usually quite slow and can often be avoided. EDIT2:正如在评论中所提到的那样,在Equals进行反射甚至GetHashCode也是一个坏主意,因为它通常很慢并且通常可以避免。 If you know the properties to be checked for eqality at compile-time you should definitly include them within those two methods as doing so gives you much more safety. 如果你知道在编译时要检查eqality的属性,你应该明确地将它们包含在这两种方法中,因为这样做会让你更安全。 When you find yourself really to need this because you have to many properties you probably have some basic problem as your class is doing too much. 当你发现自己真的需要这个时,因为你需要很多属性,你可能会遇到一些基本的问题,因为你的课程做得太多了。

You can use reflection to get the properties of the type, eg: 您可以使用反射来获取类型的属性,例如:

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));

This way, you can implement a test that fails if any properties are added. 这样,如果添加任何属性,您可以实现失败的测试。 Of course, you'd have to add new properties to the array at the beginning. 当然,您必须在开始时向数组添加新属性。 As using reflection is an expensive operation from a performance point of view, I'd propose to use it in the test, not in the comparer itself if you need to compare lots of objects. 从性能的角度来看,使用反射是一项昂贵的操作,我建议在测试中使用它,而不是在比较器中使用它,如果你需要比较大量的对象。

Please also note the use of the BindingFlags parameter so you can restrict the properties to only the public ones and the ones on instance-level. 还请注意使用BindingFlags参数,以便您可以将属性仅限制为公共属性和实例级别的属性。

Also, you can define a custom attribute that you use to mark properties that are not relevant. 此外,您还可以定义用于标记不相关的属性的自定义属性。 For example: 例如:

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

You can apply it to a property: 您可以将它应用于房产:

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

In addition, you'd have to extend the code that discovers unknown properties: 此外,您必须扩展发现未知属性的代码:

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

I guess you can check properties count inside the comparer. 我猜你可以检查比较器里面的属性数。 Something like this: 像这样的东西:

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);
        }
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM