J'ai lu que l'utilisation de "new" dans un constructeur (pour tout autre objet que celui de simple valeur) est une mauvaise pratique, car elle rend les tests unitaires impossibles (dans la mesure où ces collaborateurs doivent également être créés et ne peuvent être simulés). Comme je n'ai pas vraiment d'expérience dans les tests unitaires, j'essaie de rassembler certaines règles que je vais apprendre en premier. En outre, s’agit-il d’une règle généralement valable, quelle que soit la langue utilisée?
unit-testing
constructors
Ezoela Vacca
la source
la source
new
signifie différentes choses dans différentes langues.new
mot clé. De quelles langues parlez-vous?Réponses:
Il y a toujours des exceptions, et je suis en désaccord avec le mot "toujours" dans le titre, mais oui, cette directive est généralement valable et s'applique également à l'extérieur du constructeur.
L'utilisation de new dans un constructeur enfreint le D de SOLID (principe d'inversion de dépendance). Cela rend votre code difficile à tester car les tests unitaires sont entièrement basés sur l'isolation; il est difficile d'isoler la classe si elle a des références concrètes.
Il ne s’agit pas seulement de tests unitaires. Que faire si je veux diriger un référentiel vers deux bases de données différentes en même temps? La possibilité de passer dans mon propre contexte me permet d’instancier deux référentiels différents pointant vers des emplacements différents.
Ne pas utiliser new dans le constructeur rend votre code plus flexible. Ceci s'applique également aux langages pouvant utiliser des constructions autres que
new
pour l'initialisation d'objet.Cependant, il est clair que vous devez faire preuve de bon jugement. Il y a beaucoup de fois où il est bon d'utiliser
new
ou qu'il serait préférable de ne pas utiliser, mais vous n'aurez pas de conséquences négatives. À un moment donné,new
il faut appeler quelque part . Faites très attention lorsque vous appeleznew
dans une classe dont dépendent beaucoup d'autres.Faire quelque chose comme initialiser une collection privée vide dans votre constructeur est acceptable, et l'injecter serait absurde.
Plus une classe a de références, plus vous devriez faire attention à ne pas appeler
new
de l'intérieur.la source
head = null
à l’amélioration du code? Pourquoi laisser les collections incluses comme nulles et les créer à la demande serait mieux que de le faire dans le constructeur?Bien que je sois favorable à l'utilisation du constructeur pour initialiser simplement la nouvelle instance plutôt que pour créer plusieurs autres objets, les objets d'assistance fonctionnent bien et vous devez utiliser votre jugement pour déterminer si quelque chose est une telle assistance interne ou non.
Si la classe représente une collection, elle peut alors avoir un tableau d'assistance interne, une liste ou un hachage. Il serait utile
new
de créer ces aides et cela serait considéré comme tout à fait normal. La classe n'offre pas d'injection pour utiliser différentes aides internes et n'a aucune raison de le faire. Dans ce cas, vous souhaitez tester les méthodes publiques de l'objet, qui peuvent aller jusqu'à accumuler, supprimer et remplacer des éléments dans la collection.D'une certaine manière, la construction de classe d'un langage de programmation est un mécanisme permettant de créer des abstractions de niveau supérieur, et nous créons de telles abstractions afin de combler le fossé entre les primitives du domaine du problème et du langage de programmation. Cependant, le mécanisme de classe n'est qu'un outil; il varie selon le langage de programmation et, certaines abstractions de domaine, dans certains langages, nécessitent simplement plusieurs objets au niveau du langage de programmation.
En résumé, vous devez déterminer si l'abstraction nécessite simplement un ou plusieurs objets internes / auxiliaires, tout en restant perçu par l'appelant comme une seule abstraction, ou si les autres objets seraient mieux exposés à l'appelant pour créer pour contrôle des dépendances, ce qui serait suggéré, par exemple, lorsque l'appelant voit ces autres objets lors de l'utilisation de la classe.
la source
Tous les collaborateurs ne sont pas suffisamment intéressants pour effectuer des tests unitaires séparément, vous pouvez (indirectement) les tester via la classe d'hébergement / d'instanciation. Cela peut ne pas correspondre à l'idée de certaines personnes de devoir tester chaque classe, chaque méthode publique, etc., en particulier lors des tests suivants. Lorsque vous utilisez TDD, vous pouvez refactoriser ce "collaborateur" en extrayant une classe où elle est déjà complètement testée à partir de votre premier processus de test.
la source
Not all collaborators are interesting enough to unit-test separately
fin de l'histoire :-), Cette affaire est possible et personne n'a osé le mentionner. @Joppe, je vous encourage à élaborer un peu la réponse. Par exemple, vous pouvez ajouter quelques exemples de classes qui ne sont que des détails d'implémentation (ne pouvant pas être remplacés) et comment elles peuvent être extraites ultérieurement si nous le jugeons nécessaire.new File()
au fichier lié à quoi que ce soit, il n'y a pas de sens en interdisant cet appel. Qu'est-ce que tu vas faire, écrire des tests de régression par rapport auFile
module stdlib ? Pas probable. D'autre part, faire appelnew
à une classe auto-inventée est plus douteux.new File()
.Soyez prudent en apprenant des "règles" pour des problèmes que vous n'avez jamais rencontrés. Si vous rencontrez une "règle" ou une "meilleure pratique", je vous suggérerais de trouver un exemple simple de jouet où cette règle est "supposée" être utilisée, et d'essayer de résoudre ce problème vous - même , en ignorant ce que la "règle" dit.
Dans ce cas, vous pouvez essayer de créer 2 ou 3 classes simples et certains comportements à mettre en œuvre. Implémentez les classes de la manière qui vous semble naturelle et écrivez un test unitaire pour chaque comportement. Faites une liste de tous les problèmes que vous avez rencontrés, par exemple si vous aviez commencé avec des choses qui fonctionnaient dans un sens, vous deviez ensuite revenir en arrière et les changer plus tard; si vous avez du mal à comprendre comment les choses sont censées s'emboîter; si vous vous ennuyez en écrivant un message passe-partout; etc.
Ensuite, essayez de résoudre le même problème en suivant la "règle". Encore une fois, faites une liste des problèmes que vous avez rencontrés. Comparez les listes et réfléchissez aux situations qui pourraient être meilleures en suivant la règle et celles qui ne le pourraient pas.
En ce qui concerne votre question, j’ai tendance à privilégier une approche basée sur les ports et les adaptateurs , dans laquelle nous faisons une distinction entre "logique de base" et "services" (ceci est similaire à la distinction entre fonctions pures et procédures efficaces).
La logique de base consiste à calculer des éléments "à l'intérieur" de l'application, en fonction du domaine du problème. Il peut contenir des classes comme
User
,Document
,Order
,Invoice
, etc. Il va bien d'avoir des classes de base appellentnew
à d' autres classes de base, car ils sont les détails de mise en œuvre « internes ». Par exemple, créer unOrder
peut également créer unInvoice
et unDocument
détail de ce qui a été commandé. Il n'est pas nécessaire de vous moquer de cela pendant les tests, car ce sont les choses que nous voulons tester!Les ports et les adaptateurs permettent d’interagir entre la logique principale et le monde extérieur. C'est là des choses comme
Database
,ConfigFile
,EmailSender
, etc. vivent. Ce sont les choses qui rendent les tests difficiles, il est donc conseillé de les créer en dehors de la logique de base et de les transmettre au besoin (avec une injection de dépendance, ou comme arguments de méthode, etc.).De cette façon, la logique principale (qui est la partie spécifique à l'application, où réside la logique métier importante et est soumise au plus grand nombre de désabonnements) peut être testée seule, sans avoir à se soucier des bases de données, des fichiers, des courriels, etc. Nous pouvons simplement passer quelques exemples de valeurs et vérifier que nous obtenons les bonnes valeurs de sortie.
Les ports et les adaptateurs peuvent être testés séparément, à l'aide de modèles pour la base de données, le système de fichiers, etc., sans avoir à se soucier de la logique métier. Nous pouvons simplement passer des exemples de valeurs et nous assurer qu'elles sont stockées / lues / envoyées / etc. de manière appropriée.
la source
Permettez-moi de répondre à la question en regroupant ce que je considère être les points clés ici. Je citerai un utilisateur pour la brièveté.
Oui, l'utilisation de
new
constructeurs internes conduit souvent à des défauts de conception (par exemple, un couplage étroit) qui rend notre conception rigide. Difficile de tester oui, mais pas impossible. La propriété en jeu ici est la résilience (tolérance aux changements) 1 .Néanmoins, la citation ci-dessus n'est pas toujours vraie. Dans certains cas, certaines classes peuvent être étroitement couplées . David Arno a commenté un couple.
Exactement. Certaines classes (par exemple, les classes internes) pourraient n'être que des détails d'implémentation de la classe principale. Celles-ci sont destinées à être testées aux côtés de la classe principale et ne sont pas nécessairement remplaçables ni extensibles.
De plus, si notre culte SOLID nous oblige à extraire ces classes , nous pourrions violer un autre principe positif. La soi-disant loi de Demeter . Ce qui, au contraire, me semble très important du point de vue de la conception.
Donc, la réponse probable, comme d'habitude, dépend . Utiliser des
new
constructeurs internes peut être une mauvaise pratique. Mais pas systématiquement toujours.Il nous faut donc évaluer si les classes sont des détails d'implémentation (dans la plupart des cas, elles ne le seront pas) de la classe principale. S'ils le sont, laissez-les seuls. Si ce n'est pas le cas, envisagez des techniques telles que la composition en racine ou l' injection de dépendance par IoC Containers .
1: L’objectif principal de SOLID n’est pas de rendre notre code plus testable. C'est pour rendre notre code plus tolérant aux changements. Plus flexible et, par conséquent, plus facile à tester
Remarque: David Arno, TheWhisperCat, j'espère que cela ne vous dérange pas que je vous ai cité.
la source
Comme exemple simple, considérons le pseudocode suivant
Comme il
new
s’agit d’un détail purement lié à la mise en œuvreFoo
, et aux deuxFoo::Bar
et qui enFoo::Baz
font partieFoo
, lorsque les tests unitaires ne servent àFoo
rien de se moquer de certains élémentsFoo
. Vous ne moquez que les pièces à l' extérieurFoo
lors des tests unitairesFoo
.la source
Oui, utiliser 'new' dans votre classe racine d'application est une odeur de code. Cela signifie que vous obligez la classe à utiliser une implémentation spécifique et que vous ne pourrez pas en remplacer une autre. Toujours opter pour l'injection de la dépendance dans le constructeur. Ainsi, non seulement vous serez en mesure d’injecter facilement des dépendances simulées lors des tests, mais cela rendra votre application beaucoup plus flexible en vous permettant de remplacer rapidement différentes implémentations si nécessaire.
EDIT: Pour les utilisateurs qui descendent, voici un lien vers un livre de développement logiciel identifiant "nouvelle" comme une odeur de code possible: https://books.google.com/books?id=18SuDgAAQBAJ&lpg=PT169&dq=new%20keyword%20code%20smell&pg=PT169 # v = onepage & q = new% 20keyword% 20code% 20smell & f = false
la source
Yes, using 'new' in your non-root classes is a code smell. It means you are locking the class into using a specific implementation, and will not be able to substitute another.
Pourquoi c'est un problème? Toutes les dépendances dans un arbre de dépendance ne doivent pas toutes être remplacéesnew
mot clé n'est pas une mauvaise pratique et ne l'a jamais été. C'est la façon dont vous utilisez l'outil qui compte. Vous n'utilisez pas de marteau, par exemple, où un marteau à pointe plate suffira.