Avec les langages les plus courants (Java, C #, Java, etc.), il semble parfois que vous travaillez en désaccord avec le langage lorsque vous souhaitez entièrement TDD votre code.
Par exemple, en Java et en C #, vous voudrez simuler toutes les dépendances de vos classes et la plupart des frameworks simulateurs recommanderont de simuler les interfaces et non les classes. Cela signifie souvent que vous disposez de nombreuses interfaces avec une seule implémentation (cet effet est encore plus notable car TDD vous obligera à écrire un plus grand nombre de classes plus petites). Les solutions qui vous permettent de vous moquer correctement des classes concrètes font des choses comme modifier le compilateur ou remplacer les chargeurs de classe, etc., ce qui est assez méchant.
Alors, à quoi ressemblerait un langage s'il était conçu à partir de zéro pour être parfait avec TDD? Peut-être un moyen au niveau du langage de décrire les dépendances (plutôt que de passer des interfaces à un constructeur) et de pouvoir séparer l'interface d'une classe sans le faire explicitement?
Réponses:
Il y a plusieurs années, j'ai créé un prototype qui répondait à une question similaire; voici une capture d'écran:
L'idée était que les assertions sont en ligne avec le code lui-même, et tous les tests s'exécutent essentiellement à chaque frappe. Donc, dès que vous réussissez le test, vous voyez la méthode devenir verte.
la source
Il serait typé dynamiquement plutôt que statiquement. Le typage canard ferait alors le même travail que les interfaces dans les langages typés statiquement. De plus, ses classes seraient modifiables au moment de l'exécution afin qu'un framework de test puisse facilement stub ou simuler des méthodes sur des classes existantes. Le rubis est une de ces langues; rspec est son premier framework de test pour TDD.
Comment la dactylographie dynamique facilite les tests
Avec la saisie dynamique, vous pouvez créer des objets fantômes en créant simplement une classe qui a la même interface (signatures de méthode) que l'objet collaborateur dont vous avez besoin de se moquer. Par exemple, supposons que vous ayez eu une classe qui a envoyé des messages:
Disons que nous avons un MessageSenderUser qui utilise une instance de MessageSender:
Notez l'utilisation ici de l' injection de dépendances , un aliment de base des tests unitaires. Nous y reviendrons.
Vous souhaitez tester que les
MessageSenderUser#do_stuff
appels sont envoyés deux fois. Tout comme vous le feriez dans un langage tapé statiquement, vous pouvez créer un faux MessageSender qui compte le nombre de foissend
appelé. Mais contrairement à un langage typé statiquement, vous n'avez besoin d'aucune classe d'interface. Allez-y et créez-le:Et utilisez-le dans votre test:
En soi, le "typage canard" d'une langue typée dynamiquement n'ajoute pas grand-chose aux tests par rapport à une langue typée statiquement. Mais que se passe-t-il si les classes ne sont pas fermées, mais peuvent être modifiées au moment de l'exécution? Cela change la donne. Voyons comment.
Et si vous n'aviez pas à utiliser l'injection de dépendance pour rendre une classe testable?
Supposons que MessageSenderUser n'utilise que MessageSender pour envoyer des messages et que vous n'avez pas besoin d'autoriser la substitution de MessageSender par une autre classe. Au sein d'un même programme, c'est souvent le cas. Réécrivons MessageSenderUser pour qu'il crée et utilise simplement un MessageSender, sans injection de dépendance.
MessageSenderUser est désormais plus simple à utiliser: personne qui le crée n'a besoin de créer un MessageSender pour l'utiliser. Cela ne ressemble pas à une grande amélioration dans cet exemple simple, mais imaginez maintenant que MessageSenderUser est créé à plusieurs reprises, ou qu'il a trois dépendances. Maintenant, le système a beaucoup d'instances de passage juste pour rendre les tests unitaires heureux, pas parce qu'il améliore nécessairement la conception du tout.
Les classes ouvertes vous permettent de tester sans injection de dépendance
Un framework de test dans un langage avec typage dynamique et classes ouvertes peut rendre TDD assez agréable. Voici un extrait de code d'un test rspec pour MessageSenderUser:
C'est tout le test. Si
MessageSenderUser#do_stuff
n'appelle pasMessageSender#send
exactement deux fois, ce test échoue. La vraie classe MessageSender n'est jamais invoquée: nous avons dit au test que chaque fois que quelqu'un essaie de créer un MessageSender, il devrait obtenir notre faux MessageSender à la place. Aucune injection de dépendance nécessaire.C'est agréable de faire autant dans un test aussi simple. Il est toujours plus agréable de ne pas avoir à utiliser l'injection de dépendances à moins que cela ne soit réellement logique pour votre conception.
Mais qu'est-ce que cela a à voir avec les classes ouvertes? Notez l'appel à
MessageSender.should_receive
. Nous n'avons pas défini #should_receive lorsque nous avons écrit MessageSender, alors qui l'a fait? La réponse est que le framework de test, en apportant quelques modifications soigneuses aux classes système, est capable de le faire apparaître car à travers #should_receive est défini sur chaque objet. Si vous pensez que la modification de classes système comme celle-ci nécessite une certaine prudence, vous avez raison. Mais c'est la chose parfaite pour ce que fait la bibliothèque de tests ici, et les classes ouvertes le permettent.la source
'fonctionne bien avec TDD' n'est sûrement pas suffisant pour décrire un langage, donc il pourrait "ressembler" à n'importe quoi. Lisp, Prolog, C ++, Ruby, Python ... faites votre choix.
De plus, il n'est pas clair que le support de TDD soit quelque chose qui soit mieux géré par le langage lui-même. Bien sûr, vous pouvez créer un langage dans lequel chaque fonction ou méthode a un test associé, et vous pouvez intégrer la prise en charge de la découverte et de l'exécution de ces tests. Mais les frameworks de tests unitaires gèrent déjà bien la partie découverte et exécution, et il est difficile de voir comment ajouter proprement l'exigence d'un test pour chaque fonction. Les tests nécessitent-ils également des tests? Ou existe-t-il deux classes de fonctions - normales qui nécessitent des tests et des fonctions de test qui n'en ont pas besoin? Cela ne semble pas très élégant.
Il est peut-être préférable de prendre en charge TDD avec des outils et des cadres. Construisez-le dans l'IDE. Créez un processus de développement qui l'encourage.
De plus, si vous concevez une langue, il est bon de penser à long terme. N'oubliez pas que le TDD n'est qu'une méthode, et pas la méthode de travail préférée de tous. Il peut être difficile d'imaginer, mais il est possible que des moyens encore meilleurs se présentent. En tant que concepteur de langues, voulez-vous que les gens abandonnent votre langue lorsque cela se produit?
Tout ce que vous pouvez vraiment dire pour répondre à la question, c'est qu'une telle langue serait propice aux tests. Je sais que cela n'aide pas beaucoup, mais je pense que le problème vient de la question.
la source
Eh bien, les langages typés dynamiquement ne nécessitent pas d'interfaces explicites. Voir Ruby ou PHP, etc.
D'un autre côté, les langages typés statiquement comme Java et C # ou C ++ appliquent les types et vous obligent à écrire ces interfaces.
Ce que je ne comprends pas, c'est quel est votre problème avec eux. Les interfaces sont un élément clé de la conception et elles sont utilisées partout dans les modèles de conception et dans le respect des principes SOLIDES. Par exemple, j'utilise fréquemment des interfaces en PHP car elles rendent la conception explicite et imposent également la conception. D'un autre côté, dans Ruby, vous n'avez aucun moyen d'appliquer un type, c'est un langage typé canard. Mais encore, vous devez imaginer l'interface là-bas et vous devez abstraire la conception dans votre esprit afin de la mettre en œuvre correctement.
Ainsi, bien que votre question puisse sembler intéressante, cela implique que vous avez des problèmes avec la compréhension ou l'application des techniques d'injection de dépendance.
Et pour répondre directement à votre question, Ruby et PHP ont une excellente infrastructure de simulation à la fois intégrée dans leurs cadres de tests unitaires et livrée séparément (voir Mockery for PHP). Dans certains cas, ces frameworks vous permettent même de faire ce que vous proposez, des choses comme se moquer des appels statiques ou des initialisations d'objets sans injecter explicitement une dépendance.
la source