Nunit: Le comportement de DictionaryContainsKeyConstraint est inconstant avec Dictionary.ContainsKey lorsque le dictionnaire utilise un comparateur personnalisé

Créé le 1 mai 2018  ·  40Commentaires  ·  Source: nunit/nunit

Si vous avez un dictionnaire créé avec un comparateur personnalisé, DictionaryContainsKeyConstraint ne fonctionne pas comme prévu.

var dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { { "Hello", "World" }, { "Hola", "Mundo" } };
Assert.That(dictionary, Does.ContainKey("hello"));
done bug normal

Commentaire le plus utile

Étant donné que je suis celui qui a causé tous ces changements sous vous, je serai ravi de rebaser vos modifications et de corriger les conflits de fusion si vous le souhaitez.

Tous les 40 commentaires

Jusqu'à ce que cela soit corrigé, vous pouvez utiliser Using comme solution de contournement,

C# Assert.That(dictionary, Does.ContainKey("hello").Using((IComparer)StringComparer.OrdinalIgnoreCase));

Les tests unitaires qui échouent pour ce problème sont à #2838, actuellement marqués comme explicites.

Je peux y jeter un œil, j'ai une branche locale et j'ai les tests unitaires qui ont été joliment ajoutés par Eamon.

Ça a l'air génial, merci @DaiMichael !

Cependant, d'après un rapide coup d'œil au code sur mon téléphone, il semble que nous soyons en territoire de « rupture de changement » ici.

DictionaryContainsKeyConstraint utilise actuellement NUnitEqualityComparer, plutôt que le comparateur par défaut pour un type comme le ferait un dictionnaire. NUnitEqualityComparer a divers ajouts qui permettent à certains types qui ne seraient pas égaux à .NET Equals() normaux d'être « égaux ».

Compte tenu de la nature de la contrainte, mon avis pour le moment est que nous devrions effectuer le changement afin que cette contrainte utilise toujours le comparateur du dictionnaire - malgré le fait que cela puisse casser certains tests utilisateurs existants. Cela vaut probablement la peine d'attendre que les autres sur @nunit/framework-team interviennent cependant, sur ce qu'ils pensent être la meilleure façon de prendre cela !

Comme je l'ai dit, je n'ai regardé que sur mon téléphone, donc tout est peut-être faux !

@ChrisMaddock

Donc, en regardant un peu plus, au premier coup d'œil, je chercherais à vérifier si le dictionnaire transmis a un EqualityComparer<TKey>.Default . Si tel est le cas, nous pouvons utiliser le « NUnitEqualityComparer » actuel, sinon nous utilisons celui défini dans le dictionnaire, qui en théorie devrait être celui souhaité.

Je n'ai pas encore vu si cela casserait des tests, mais cela ne devrait changer de comportement que lorsque le cas d'utilisation l'exige ?! Célèbres derniers mots évidemment. Je vais faire le changement localement et voir ce qui sort au lavage...

@DaiMichael Je suis d'accord que cela éviterait des tests cassés - mais je crains que cela ne complique beaucoup le "résultat attendu" pour éviter le changement radical. Personnellement, je suis actuellement en train de pécher par excès du côté du changement radical - mais je tiens à savoir ce que les autres pensent. ??

Ce serait cool de voir un test que le changement va casser. Peut-être que deux d'entre eux peuvent se casser dans les deux sens.

Je préfère faire le changement radical. Cependant, il semble erroné de devoir lire Dictionary<,>.Comparer . Que se passe-t-il si mon type implémente IDictionary et délègue à une instance de dictionnaire interne ?

Pouvons-nous plutôt reconnaître le IDictionary / IDictionary<,> / IReadOnlyDictionary<,> / ILookup<,> / KeyedCollection<,> et appeler le Contains(TKey) ou ContainsKey(object ) méthode?
Étant donné le nom de la contrainte, que se passe-t-il si nous duck-type et utilisons la méthode ContainsKey d'instance publique renvoyant un bool qui prend un seul paramètre ? Ensuite, si aucune méthode publique de ce type n'existe, recherchez la même méthode sur toutes les interfaces implémentées ? Quelque chose comme ça?

Je vais chercher à procéder de la manière non facile 😉

@CharliePoole @jnm2 @ChrisMaddock

J'ai fait le changement, pris sans vergogne le code @jnm2 posté pour rechercher le ContainsKey() . Cela fonctionne et j'ajouterai quelques tests pour cela si nous continuons à utiliser ce code comme moyen de résoudre le problème.

Le nouveau code corrige les 2 nouveaux tests, ouais ! ??
Il casse 3 tests 😞.

Code sur ma branche
https://github.com/DaiMichael/nuni/blob/issue-2837/src/NUnitFramework/framework/Constraints/DictionaryContainsKeyConstraint.cs

Essais
https://github.com/DaiMichael/nuni/blob/issue-2837/src/NUnitFramework/tests/Constraints/DictionaryContainsKeyConstraintTests.cs

Les échecs
1) FailsWhenNotUsedAgainstADictionary -> Ceci est une liste de KeyValuePair et elle utilise KeyValuePairsComparer de NUnitEqualityComparer .
2) IgnoreCaseIsHonored utilise DictionaryContainsKeyConstraint.IgnoreCase qui est totalement ignoré par le ContainsKey() .
3) UsingIsHonored utilise DictionaryContainsKeyConstraint.Using qui ajoute le Func<> à NUnitEqualityComparer et appelle maintenant ContainsKey() .

Donc, je suppose que cela revient à savoir si nous voulons casser les cas d'utilisation actuels ? Nous pourrions utiliser par défaut l'ancienne méthode si nous ne trouvons pas ContainsKey() mais que personne ne semblait enthousiaste.

Je suis tout à fait d'accord sur ce que les gens pensent être la meilleure façon de procéder ?

Est-ce que FailsWhenNotUsedAgainstADictionary échoue parce que le message a changé ou parce qu'il ne lève plus d'exception ?

IgnoreCase est contre-intuitif pour moi. Quel est le cas d'utilisation d'un comparateur qui entre en conflit avec le dictionnaire pour vérifier l'existence d'une clé ? Une approche sémantiquement plus appropriée serait d'utiliser une assertion de collection sur dictionary.Keys puisque le fait qu'il s'agisse d'un dictionnaire est un faux-fuyant.

La meilleure façon de procéder est probablement de déprécier les options .IgnoreCase et .Using, ou bien de conserver en interne un deuxième chemin de code. Plutôt que de conserver le code existant, il est tentant pour moi de générer une assertion de collection comme Has.Property("Keys").With.Some.EqualTo(expected).IgnoreCase etc.

@rprouse Qu'en pensez-vous jusqu'à présent ? Mon homme de paille ContainsKey est-il une bonne chose et devrions-nous conserver le comportement existant si .IgnoreCase est touché ?

@DaiMichael Si nous faisons cela, j'aimerais voir/contribuer à des tests montrant le nouveau code ContainsKey fonctionnant avec :

  • une classe qui hérite de object et implémente IReadOnlyDictionary<,>
  • idem pour IDictionary<,>
  • idem pour IDictionary
  • idem pour ISet<> (test négatif, car les sets n'ont pas de clés)
  • idem pour ILookup<,>
  • une classe qui hérite de KeyedCollection<,>
  • une classe qui hérite de l'objet, n'implémente aucune interface et déclare une méthode publique ContainsKey
  • une classe qui hérite de Dictionary<,> , n'implémente aucune interface supplémentaire et déclare une nouvelle méthode publique ContainsKey qui agit différemment de la base.
  • une classe qui hérite de l'objet, n'implémente aucune interface et déclare une méthode publique Contains(TKey)

@jnm2 FailsWhenNotUsedAgainstADictionary échoue car le message ArgumentException a changé, ce serait facile à changer pour le type d'entrée (c'est-à-dire IDictionary, ISet, etc...).

Je suis d'accord sur le fait que j'ajouterais beaucoup plus de cas de test comme vous le mentionnez, mais je voulais évaluer ce qu'était la fin du jeu avant d'écrire beaucoup de tests, uniquement pour qu'ils soient rejetés si nous changions de tact.

Je vois, merci !
C'est la stratégie à laquelle je m'attendais. (Au fait, « changer de point de vue » est l'une de ces phrases auxquelles vous devez faire attention ! )

Je suis d'accord avec tous vos commentaires, @jnm2. Pour moi, DictionaryContainsKeyConstraint teste une chose très spécifique, donc d'autres contraintes devraient être utilisées pour tester d'autres choses !

Plutôt que de conserver le code existant, il est tentant pour moi de générer une assertion de collection comme Has.Property("Keys").With.Some.EqualTo(expected).IgnoreCase

J'aime particulièrement cette idée ! Mais pensez que nous devrions déprécier si nous optons pour cela, car cela pourrait créer un comportement inattendu pour quiconque écrivant de nouveaux tests ! Le but de ce changement, à mes yeux, serait d'éviter de casser la compilation des tests existants.

@ChrisMaddock Je pensais que nous retournerions toujours un DictionaryContainsKeyConstraint qui déléguerait en interne à Has.Property("Keys").With.Some.EqualTo(expected).IgnoreCase pour la compatibilité, et que le comportement de réussite/échec résultant serait identique à ce que nous faisons aujourd'hui. Mais si je me trompe, tant pis. :RÉ

@jnm2 ISet<> ne prend pas en charge ContainsKey , il prend en charge Contains mais alors chaque classe de base de collection le fait ala List et il testera simplement l'égalité entre les éléments. Cela ne semble pas pertinent dans le cas des tests DictionaryContainsKey ?

Je ne pense pas que nous devrions tester dans ce cas et cela ne fonctionne pas actuellement car Duck Type ne peut pas trouver un ContainsKey avec un TKey générique, c'est donc un cas d'utilisation différent.

@jnm2 @ChrisMaddock

D'accord, j'ai vérifié les tests mais ce n'est pas terminé car j'en ai quelques-uns sur Ignore .

Selon le commentaire ci-dessus, ISet<> ne semble pas un cas de test valide car ce n'est pas vraiment une collection "à clé". Le "Contient" vérifierait si l'élément est dans l'ensemble, pas qu'il s'agit d'une clé. Je ne l'ai pas fait.

IDictionary' fails as it doesn't contain Contient or ContainsKey`, c'est parce que nous recherchons une méthode avec un paramètre TKey générique, je pourrais ajuster la recherche pour retirer l'un ou l'autre et ignorer la contrainte générique que nous avons dessus.

Les 2 tests qui implémentent simplement les Contains et ContainsKey ne fonctionnent pas comme l'interface CollectionConstraint' expects there to be an IEnumerable`, donc échec en dehors de ce changement et peut-être valide. Nous pourrions le faire en tant que test, mais nous attendons à lancer une exception. Pas fait actuellement car non valide ?

Enfin... 😓 J'ai toujours les deux tests du premier enregistrement IgnoreCase flag et Using car je ne savais pas encore quoi faire ici.

Alors les questions sont
1) Dois-je modifier le type de canard pour obtenir Contains ou ContainsKey et ignorer si TKey est générique et obtenir simplement la méthode.
2) Que faire avec le drapeau IgnoreCase et Using

https://github.com/DaiMichael/nuni/tree/issue-2837

@DaiMichael

ISet<> ne prend pas en charge ContainsKey

Désolé, je n'ai pas expliqué ce que je voulais dire ! Nous sommes sur la même longueur d'onde. C'est pourquoi j'ai proposé d'ajouter un test négatif : de cette façon, nous montrons que nous ne le soutenons pas intentionnellement.
Cela aide-t-il à répondre à votre question 1 ?

Avec la question 2, je n'ai pas un fort penchant sur celui-ci. Soit nous gardons l'implémentation d'origine sur laquelle nous replier, soit nous rendons définitivement obsolètes les méthodes que l'équipe n'a pas voulu faire lorsque l'obsolescence dure a été suggérée dans le passé. @ChrisMaddock , @rprouse ?

Je n'avais pas voulu dire que vous deviez hériter explicitement de object , juste que vous ne devriez pas hériter d'une classe qui a déjà implémenté une interface. Mais c'est cool.

@jnm2 Ça a été une longue semaine de travail... J'étais juste stupide sur les tests négatifs et en fait les objets simples avec Contains et ConatinsKey sont aussi de bons tests négatifs...

Je reviendrai... idem sur le ISet<> ...

Je vais regarder la marque Obsolete et rendre la rétrocompatibilité conforme aux modifications précédentes.

Je vais jeter un œil à la marque Obsolète et la rendre rétrocompatible conformément aux modifications précédentes.

Je suis d'accord c'est préférable

@ChrisMaddock @jnm2

J'ai terminé les tests unitaires supplémentaires mais je ne me suis pas encore engagé.

Marquer Using et IgnoreCase comme [Deprecated] est un peu collant. Ils sont tous implémentés dans la classe CollectionItemsEqualConstraint .

Je ne peux pas penser à un moyen d'autoriser la rétrocompatibilité et de la marquer [Deprecated] sans :
1) Rendez les méthodes dans CollectionItemsEqualConstraint virtuelles et remplacez-les (appelez la méthode base mais ajoutez le point 2).
2) Créez un indicateur _obsoleteFlag qui utilise ensuite l'ancienne méthode lorsqu'elle est définie via les méthodes surchargées.

Cela fonctionnera évidemment mais n'est pas particulièrement joli 😞

De meilleures pensées.

@DaiMichael Nous ferions l'obsolescence en déclarant des méthodes d'ombrage sur la classe DictionaryContainsKeyConstraint via le mot-clé new . Il est très peu probable que DictionaryContainsKeyConstraint soit utilisé de manière polymorphe via CollectionItemsEqualConstraint .

@jnm2 - J'avais commencé cette approche. Je vous pose peut-être trop de questions... C'est juste que je ne veux pas faire perdre de temps à qui que ce soit en faisant des pull-requêtes qui ne sont pas voulues.

Nous avons donc maintenant un tas de tests supplémentaires comme demandé et j'ai déprécié les autres appels utilisés dans les tests avec une rétrocompatibilité pour les appels dépréciés.

Je vais faire une pull request maintenant... :wink:

@DaiMichael Vous ne vous [wip] dans le titre afin de mettre du code devant nous, si vous le souhaitez.

Nous avons déjà expliqué comment hériter de CollectionConstraint est une abstraction qui fuit. @DaiMichael a attiré notre attention sur le fait que CollectionConstraint.ApplyTo lève ArgumentException pour les choses qui ne sont pas IEnumerable :

https://github.com/nuni/nunit/blob/46b62b6dc4087efd0b4758ea039f3e07e7a7b911/src/NUnitFramework/framework/Constraints/CollectionConstraint.cs#L71 -L75

C'est un problème si nous voulons permettre à DictionaryContainsKeyConstraint de fonctionner sur des dictionnaires qui n'implémentent pas d'interfaces.

Devrions-nous surcharger ApplyTo pour nous débarrasser de cette vérification, ou devrions-nous exiger l'interface IEnumerable comme marqueur avant de taper et d'utiliser la méthode ContainsKey ?

Pour jouer l'avocat du diable

Est-ce qu'un objet simple (selon le test) est un Dictionary simplement parce qu'il implémente soit la méthode ContainsKey et soit la méthode Contains . Je ne dis pas que ce ne sont pas des traits valides à prendre en charge, mais dans le contexte de DictionaryContainsKeyConstraint ?

La syntaxe que les gens liront est Contains.Key , donc je m'attendrais à ce que cela fonctionne sur KeyedCollection ou ILookup qui ne sont pas des dictionnaires. Une fois sur place, pourquoi ne pas laisser le type décider si vous pouvez lui demander s'il contient une clé ?

Point très juste 👍, car je pense à partir du DictionaryContainsKeyConstraint inférieur plutôt qu'au cas d'utilisation réel de Contains.Key . Ce qui revient ensuite à mon

Je ne dis pas que ce ne sont pas des traits valides à soutenir

Donc, là où nous soutenons cela dans le cadre, c'est muet dans une certaine mesure.

Devrions-nous surcharger ApplyTo pour nous débarrasser de cette vérification, ou devrions-nous exiger l'interface IEnumerable comme marqueur avant de taper et d'utiliser la méthode ContainsKey ?

J'ai implémenté cela et enregistré. J'ai remplacé la base CollectionConstraint dans DictionaryContainsKeyConstraint car je pense que la version abstraite est juste compte tenu du nom de la classe. Cela maintiendra également la compatibilité descendante pour 6 autres classes qui héritent de cette classe abstraite.

.... Je peux toujours revenir si décidé à la dernière minute ...

J'étais confus lorsque vous avez dit surchargé, car l'ombrage et le remplacement sont des choses différentes, mais en regardant le code, je vois que nous sommes sur la même page. =)

@ChrisMaddock Cela devrait être rouvert jusqu'à ce que https://github.com/DaiMichael/nuni/tree/issue-2837 soit rebasé dans une nouvelle demande d'extraction, non?

Ça devrait, désolé !

@ChrisMaddock -

Aussi - En ce qui concerne les tests "en échec". Je n'ai pas de problème localement mais je suis un peu confus car dans nunit.framework nous référençons standard mais dans nunit.framework.test nous référençons netcore ? Je ne comprends pas ça....

Oui s'il vous plaît @DaiMichael !

Aussi - En ce qui concerne les tests "en échec". Je n'ai pas de problème localement

C'est étrange! Le problème que je pensais que nous avions avant, la version CI va l'attraper - nous devrions donc être en mesure de voir si les problèmes persistent.

nunit.framework nous référençons standard mais dans nunit.framework.test nous référençons netcore ? Je ne comprends pas ça....

Je ne suis vraiment pas la meilleure personne pour expliquer cela... mais .NET Standard est l'"API" que ces builds ciblent - mais les tests doivent être exécutés sur une plate-forme réelle. Les plates-formes sur lesquelles nous exécutons ces tests sont .NET Core.

@ChrisMaddock - d'accord, ignorez tout le dernier message... Je travaille sur ce sujet... postez la hâte mais la confusion/l'incompréhension de github lorsque j'ai commencé à aider (sic) signifie que j'ai une branche polluée (sur ce problème) et J'ai un autre changement (dans une version antérieure) là-dedans. J'ai passé la journée à être plutôt frustrée mais j'ai démêlé le désordre mais je dois maintenant supprimer le mauvais commit de cette branche. Je pourrai alors rebaser (correctement) et je soupçonne que je verrai les tests unitaires défaillants. J'espère une journée passionnante à faire cela demain. Heureusement que je viens d'avoir un hip op (non, je ne suis pas si vieux ;)) et je suis limité (c'est-à-dire que je ne peux pas) sur mon vélo !

Je ne suis vraiment pas la meilleure personne pour expliquer cela... mais .NET Standard est l'"API" que ces builds ciblent - mais les tests doivent être exécutés sur une plate-forme réelle. Les plates-formes sur lesquelles nous exécutons ces tests sont .NET Core.

Je comprends comment tout cela fonctionne, je pense que le problème de branche d'en haut faisait un gâchis, je viens juste de plonger mon orteil dans Standard/Core/Framework récemment, donc je suis aussi un peu noob et facilement confus.

Gah - Je sais que je me sens bien... Je suis un homme mercuriel, donc je me retrouve régulièrement dans un pétrin avec git dès que quelque chose de non trivial se produit ! J'espère que vous pourrez régler les choses - faites-nous savoir si nous pouvons vous aider! ??

Étant donné que je suis celui qui a causé tous ces changements sous vous, je serai ravi de rebaser vos modifications et de corriger les conflits de fusion si vous le souhaitez.

@jnm2

Étant donné que je suis celui qui a causé tous ces changements sous vous, je serai ravi de rebaser vos modifications et de corriger les conflits de fusion si vous le souhaitez.

Je suis sûr à 99,9% que c'est de ma faute. J'ai un très vieux check-in du numéro 2786, j'ai dû commiter une version très ancienne dans cette branche, maintenant j'ai démêlé mes branches et mon master... Je devrais pouvoir supprimer le mauvais commit.

Si j'ai encore du mal d'ici demain, je pourrais vous blâmer et demander de l'aide, mais croisons les doigts.

@ChrisMaddock - amen à mecurial...

Nouveau PR #2967

Cette page vous a été utile?
0 / 5 - 0 notes