2017-07-06 2 views
4

J'ai une classe et un comparateur pour cette classe qui implémente IEqualityComparer:Comment implémenter des tests unitaires sur 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(); 
} 

Les deux méthodes Equals et GetHashCode sont utilisés par exemple dans List.Except par une instance du comparateur.

Ma question est: comment implémenter correctement les tests unitaires sur ce comparateur? Je veux détecter si quelqu'un ajoute une propriété publique dans Foo sans modifier le comparateur, car dans ce cas le comparateur devient invalide.

Si je fais quelque chose comme:

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

Cela ne peut pas détecter qu'une nouvelle propriété a été ajoutée, et que cette propriété est différente dans les deux objets.

Y at-il un moyen de le faire?

Si c'est possible, pouvons-nous ajouter un attribut à une propriété pour dire que cette propriété n'est pas pertinente dans la comparaison?

+0

Vous ne pouvez pas attendre votre comparateur pour comparer les propriétés, il ne sait pas qu'ils existent. Vous devez changer le comparateur pour vérifier également les nouvelles propriétés. Vous pourriez y parvenir en utilisant la réflexion mais cela rendrait votre comparateur incroyablement lent. – HimBromBeere

+1

'Foo' devrait être immuable si vous avez l'intention de l'utiliser comme clé dans les tables de hachage. Dans ce cas, les nouvelles propriétés doivent être définies en utilisant les paramètres du ctor, et tout le monde verrait que votre test pour le Comparer ne se compile plus. (Et j'espère mettre à jour le comparateur et le test). Un autre moyen facile, ajoutez simplement un commentaire dans Foo pour que Comparer XXX ait besoin d'être mis à jour. – Magnus

+0

@Magnus Cela semble intéressant, mais comment faire cela? – Boiethios

Répondre

1

Vous pouvez utiliser la réflexion pour obtenir les propriétés du type, par exemple .:

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

De cette façon, vous pouvez mettre en œuvre un test qui échoue si les propriétés sont ajoutées. Bien sûr, vous devrez ajouter de nouvelles propriétés au tableau au début. Comme l'utilisation de la réflexion est une opération coûteuse du point de vue des performances, je proposerais de l'utiliser dans le test, pas dans le comparateur même si vous avez besoin de comparer beaucoup d'objets.

Veuillez également noter l'utilisation du paramètre BindingFlags afin de pouvoir limiter les propriétés uniquement aux propriétés publiques et celles au niveau de l'instance.

De plus, vous pouvez définir un attribut personnalisé que vous utilisez pour marquer les propriétés qui ne sont pas pertinentes. Par exemple:

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

Vous pouvez l'appliquer à une propriété:

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

En outre, il faudrait étendre le code qui découvre des propriétés inconnues:

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

Je préfère votre solution, car elle ne provoque aucun temps d'exécution et les réservoirs ne sont placés que lors des tests. – Boiethios

1

Fondamentalement, vous pouvez vérifier toutes les propriétés que vous souhaitez vérifier Equals par réflexion. Pour filtrer certains d'entre eux utiliser un attribut sur ces propriétés:

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

maintenant votre chèque de comparateur pour cet attribut spécifique. Ensuite comparer tous les biens qui figure dans la liste restante par 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; 
    } 
} 

Cependant, vous devriez avoir quelques bons pré-contrôles dans les GetHashCode pour éviter les appels unneccessary à cette méthode lente.

EDIT: Comme vous l'avez mentionné ReSharper, je suppose que si vous fournissez les propriétés réelles à valider lors de l'exécution, même R # ne connaît pas un bon moyen de mettre en œuvre GetHashCode. Vous aurez besoin de propriétés qui seront toujours disponibles sur votre type et qui fournissent une assez bonne idée de ce qui pourrait être considéré comme égal. Cependant, toutes les propriétés additionnelles devraient seulement entrer dans la méthode coûteuse Equals.

EDIT2: Comme mentionné dans les commentaires faire de la réflexion au sein de Equals ou même GetHashCode est une mauvaise idée car elle est généralement assez lente et peut souvent être évitée. Si vous connaissez les propriétés à vérifier pour eqality au moment de la compilation, vous devez les inclure définitivement dans ces deux méthodes car cela vous donne beaucoup plus de sécurité. Lorsque vous vous trouvez vraiment besoin de cela parce que vous avez de nombreuses propriétés, vous avez probablement un problème de base car votre classe en fait trop.

+0

Je laisse Resharper implémenter le calcul de hachage. Cela devrait être bon. – Boiethios

+0

@Boiethios Voir ma modification. – HimBromBeere

+0

J'éviterais fortement d'utiliser la réflexion dans Equals ou GetHashCode. Ils sont indentés pour être rapides et ce n'est pas le cas. – Magnus

1

Je suppose que vous pouvez vérifier le nombre de propriétés à l'intérieur du comparateur. Quelque chose comme ceci:

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

Cette solution semble bonne, mais les méthodes 'Equals' seront plus lentes (et j'appellerai cela une quantité massive d'objets). – Boiethios

+0

J'ai une idée de comment l'accélérer. Une minute –

+0

@Boiethios, voir ma mise à jour –