Je tente de prendre l'habitude d'écrire des tests unitaires régulièrement avec mon code, mais je l' ai lu que la première , il est important d'écrire le code testable . Cette question concerne les principes SOLID de la rédaction de code testable, mais je veux savoir si ces principes de conception sont bénéfiques (ou du moins ne sont pas préjudiciables) sans que l’écriture de tests ne soit nécessaire. Pour clarifier - je comprends l'importance d'écrire des tests; Ce n'est pas une question sur leur utilité.
Pour illustrer ma confusion, dans l'article qui a inspiré cette question, l'auteur donne un exemple de fonction qui vérifie l'heure actuelle et renvoie une valeur en fonction de l'heure. L’auteur pointe ce code comme étant incorrect car il produit les données (le temps) qu’il utilise en interne, ce qui le rend difficile à tester. Pour moi, cependant, il semble exagéré de passer le temps comme argument. À un moment donné, la valeur doit être initialisée et pourquoi pas au plus près de la consommation? De plus, l'objectif de la méthode dans mon esprit est de renvoyer une valeur en fonction de l' heure actuelle , en en faisant un paramètre qui implique que cet objectif peut / doit être modifié. Ceci, ainsi que d'autres questions, m'amène à me demander si un code testable était synonyme de "meilleur" code.
L'écriture de code testable reste-t-elle une bonne pratique même en l'absence de tests?
Le code testable est-il réellement plus stable? a été suggéré comme un duplicata. Cependant, cette question concerne la "stabilité" du code, mais je demande plus généralement si le code est supérieur pour d'autres raisons également, telles que la lisibilité, les performances, le couplage, etc.
la source
func(X)
retour"Morning"
, le remplacement de toutes les occurrences defunc(X)
with"Morning"
ne changera pas le programme (c.-à-d. Que l'appelfunc
ne fait rien que renvoyer la valeur). Idempotency implique soit çafunc(func(X)) == X
(ce qui n’est pas le type correct), soit celafunc(X); func(X);
produit les mêmes effets secondaires quefunc(X)
(mais il n’ya pas d’effets secondaires ici)Réponses:
En ce qui concerne la définition commune des tests unitaires, je dirais non. J'ai vu des codes simples compliqués du fait de la nécessité de les modifier pour les adapter au cadre de test (par exemple, les interfaces et l' IoC rendent partout difficile la tâche de suivre des couches d'appels d'interface et des données qui devraient être évidentes transmises par magie). Étant donné le choix entre un code facile à comprendre et un code facile à tester à l’unité, j’utilise le code maintenable à chaque fois.
Cela ne signifie pas ne pas tester, mais adapter les outils à votre convenance et non l'inverse. Il existe d'autres moyens de tester (mais un code difficile à comprendre est toujours un mauvais code). Par exemple, vous pouvez créer des tests unitaires moins granulaires (par exemple , l’attitude de Martin Fowler selon laquelle une unité est généralement une classe, pas une méthode), ou vous pouvez utiliser votre programme avec des tests d’intégration automatisés. Ce n'est peut-être pas aussi beau que votre cadre de test s'allume avec des ticks verts, mais nous recherchons du code testé, pas la gamification du processus, n'est-ce pas?
Vous pouvez simplifier la maintenance de votre code tout en optimisant les tests unitaires en définissant de bonnes interfaces entre eux, puis en écrivant des tests qui exploitent l'interface publique du composant; ou vous pourriez obtenir un meilleur cadre de test (un système qui remplace les fonctions lors de l'exécution pour les simuler, au lieu d'exiger que le code soit compilé avec des simulacres en place). Une meilleure infrastructure de test unitaire vous permet de remplacer la fonctionnalité système GetCurrentTime () par la vôtre, au moment de l'exécution, de sorte que vous n'avez pas besoin d'introduire des wrappers artificiels dans cette application uniquement pour correspondre à l'outil de test.
la source
Tout d’abord, l’absence de tests est un problème beaucoup plus important que votre code testable ou non. Ne pas avoir de tests unitaires signifie que vous n'en avez pas fini avec votre code / fonctionnalité.
Cela dit, je ne dirais pas qu'il est important d'écrire du code pouvant être testé - il est important d'écrire du code flexible . Le code inflexible est difficile à tester, il y a donc beaucoup de chevauchements dans l'approche et ce que les gens appellent cela.
Donc pour moi, il y a toujours un ensemble de priorités dans l'écriture de code:
Les tests unitaires facilitent la maintenabilité du code, mais seulement jusqu'à un certain point. Si vous rendez le code moins lisible ou plus fragile pour faire fonctionner les tests unitaires, cela devient contre-productif. Le "code à tester" est généralement du code flexible, ce qui est bien, mais pas aussi important que la fonctionnalité ou la maintenabilité. Pour quelque chose comme l'heure actuelle, rendre cette flexibilité flexible est une bonne chose, mais cela nuit à la facilité de maintenance en rendant le code plus difficile à utiliser correctement et plus complexe. Étant donné que la maintenabilité est plus importante, je privilégierai généralement l'approche la plus simple, même si elle est moins vérifiable.
la source
Oui, c'est une bonne pratique. La raison en est que la testabilité n’est pas faite pour des tests. C’est pour des raisons de clarté et de compréhensibilité que cela apporte.
Personne ne se soucie des tests eux-mêmes. Il est triste de constater que nous avons besoin de vastes suites de tests de régression, car nous ne sommes pas assez brillants pour écrire du code parfait sans vérifier en permanence notre position. Si nous le pouvions, le concept de test serait inconnu et tout cela ne poserait pas de problème. J'aimerais bien pouvoir le faire. Mais l'expérience a montré que la quasi-totalité d'entre nous ne le peut pas. Par conséquent, les tests couvrant notre code sont une bonne chose, même s'ils réduisent le temps nécessaire à l'écriture de code d'entreprise.
Comment les tests améliorent-ils notre code d'entreprise indépendamment du test? En nous obligeant à segmenter nos fonctionnalités en unités faciles à démontrer. Ces unités sont également plus faciles à obtenir que celles que nous serions tentés d’écrire.
Votre exemple de temps est un bon point. Tant que vous ne disposez que d’une fonction renvoyant l’heure actuelle, vous pouvez penser qu’il est inutile de la programmer. Comment peut-il être difficile d'obtenir ce droit? Mais inévitablement votre programme utiliser cette fonction dans un autre code, et que vous voulez vraiment tester ce code sous différentes conditions, y compris à des moments différents. Par conséquent, il est judicieux de pouvoir manipuler l'heure renvoyée par votre fonction - non pas parce que vous vous méfiez de votre
currentMillis()
appel sur une ligne , mais parce que vous devez vérifier les appelants de cet appel dans des circonstances contrôlées. Vous voyez donc qu'avoir un code testable est utile même si, à lui seul, il ne semble pas mériter autant d'attention.la source
Nobody cares about the tests themselves
-- Je fais. Je trouve que les tests constituent une meilleure documentation de l’utilisation du code que les commentaires ou les fichiers Lisez-moi.Parce que vous devrez peut-être réutiliser ce code, avec une valeur différente de celle générée en interne. La possibilité d'insérer la valeur que vous allez utiliser en tant que paramètre vous permet de générer ces valeurs à tout moment, et pas seulement "maintenant" (avec "maintenant" signifiant lorsque vous appelez le code).
Rendre le code testable signifie en réalité créer un code qui peut (dès le début) être utilisé dans deux scénarios différents (production et test).
Fondamentalement, bien que vous puissiez affirmer qu'il n'y a aucune incitation à rendre le code testable en l'absence de tests, l'écriture de code réutilisable présente un grand avantage, et les deux sont synonymes.
Vous pouvez également faire valoir que le but de cette méthode est de renvoyer une valeur basée sur une valeur temporelle et que vous en avez besoin pour générer cette valeur en fonction de "maintenant". L’un d’eux est plus flexible, et si vous avez l’habitude de choisir cette variante, votre taux de réutilisation de code augmentera avec le temps.
la source
Cela peut sembler idiot de le dire ainsi, mais si vous voulez pouvoir tester votre code, alors oui, écrire du code testable est préférable. Tu demandes:
C’est précisément parce que, dans l’exemple que vous citez, le code est indestructible. Sauf si vous n'exécutez qu'un sous-ensemble de vos tests à différents moments de la journée. Ou vous réinitialisez l'horloge système. Ou une autre solution de contournement. Ce qui est pire que simplement rendre votre code flexible.
En plus d'être inflexible, cette petite méthode en question a deux responsabilités: (1) obtenir l'heure système puis (2) restituer une valeur en fonction de celle-ci.
Il est judicieux de décomposer davantage les responsabilités afin que la partie de votre contrôle (
DateTime.Now
) ait le moins d’impact sur le reste de votre code. Cela simplifiera le code ci-dessus et aura pour effet secondaire de pouvoir être systématiquement testé.la source
Cela a certes un coût, mais certains développeurs sont tellement habitués à le payer qu'ils en ont oublié le coût. Pour votre exemple, vous avez maintenant deux unités au lieu d'une, vous avez besoin du code d'appel pour initialiser et gérer une dépendance supplémentaire, et bien que ce
GetTimeOfDay
soit plus testable, vous êtes de retour dans le même bateau pour tester votre nouvelleIDateTimeProvider
. C'est simplement que si vous avez de bons tests, les avantages l'emportent généralement sur les coûts.En outre, dans une certaine mesure, écrire du code testable vous encourage en quelque sorte à concevoir votre code de manière plus facile à gérer. Le nouveau code de gestion des dépendances est gênant. Vous devrez donc regrouper toutes vos fonctions dépendantes du temps, si possible. Cela peut aider à atténuer et à corriger les bogues, par exemple lorsque vous chargez une page à droite dans un intervalle de temps donné, certains éléments étant rendus à l'aide de l'heure d'avant et d'autres à l'aide de l'heure de retour. Il peut également accélérer votre programme en évitant les appels système répétés pour obtenir l'heure actuelle.
Bien entendu, ces améliorations architecturales dépendent fortement de la prise de conscience des opportunités et de leur mise en œuvre. L'un des plus grands dangers de se focaliser aussi étroitement sur les unités est de perdre de vue la vue d'ensemble.
De nombreux frameworks de tests unitaires vous permettent de patcher un objet fictif au moment de l'exécution, ce qui vous permet de profiter des avantages de la testabilité sans tout le désordre. Je l'ai même vu faire en C ++. Examinez cette possibilité dans des situations où il semble que le coût de la testabilité n'en vaut pas la peine.
la source
Il est possible que toutes les caractéristiques contribuant à la testabilité ne soient pas souhaitables en dehors du contexte de testabilité - j'ai du mal à trouver une justification non liée au test pour le paramètre de temps que vous citez, par exemple - mais en gros aux caractéristiques contribuant à la testabilité. contribuent également à un bon code indépendamment de la testabilité.
De manière générale, le code testable est un code malléable. Il s'agit de petits morceaux discrets et cohérents, de sorte que des bits individuels peuvent être appelés pour être réutilisés. C'est bien organisé et bien nommé (pour pouvoir tester certaines fonctionnalités, vous devez accorder plus d'attention à la dénomination; si vous n'écriviez pas, le nom d'une fonction à usage unique importerait moins). Il a tendance à être plus paramétrique (comme votre exemple de temps), donc ouvert à une utilisation dans d'autres contextes que l'objectif initial. C'est sec, donc moins encombré et plus facile à comprendre.
Oui. C'est une bonne pratique d'écrire du code testable, même indépendamment du test.
la source
L'écriture de code testable est importante si vous voulez pouvoir prouver que votre code fonctionne réellement.
J'ai tendance à être d'accord avec les sentiments négatifs à propos de la déformation de votre code en contorsions odieuses juste pour l'adapter à un cadre de test particulier.
D'un autre côté, tout le monde ici a, à un moment ou à un autre, dû faire face à cette fonction magique longue de 1 000 lignes qu'il est horrible d'avoir à traiter, qui ne peut pratiquement pas être touchée sans rompre une ou plusieurs tâches obscures non liées. dépendances évidentes quelque part ailleurs (ou quelque part en elle-même, où la dépendance est presque impossible à visualiser) et est par définition à peu près indestimable. La notion (qui n'est pas dénuée de mérite) que les frameworks de test ont été surchargés ne doit pas être considérée comme une licence libre pour écrire du code de mauvaise qualité et indestimable, à mon avis.
Les idéaux de développement axés sur les tests ont tendance à vous pousser à écrire des procédures à responsabilité unique, par exemple, et c'est certainement une bonne chose. Personnellement, je dis: engagez-vous dans une responsabilité unique, une source unique de vérité, une étendue contrôlée (aucune variable globale foutue) et gardez les dépendances fragiles au minimum, et votre code sera testable. Testable par un framework de test spécifique? Qui sait. Mais alors, c’est peut-être le cadre de test qui doit s’ajuster à un bon code, et non l’inverse.
Mais juste pour être clair, un code si intelligent, ou si long et / ou si interdépendant qu’il n’est pas facilement compris par un autre être humain, n’est pas un bon code. Et comme par coïncidence, le code ne peut pas être facilement testé.
Si près de mon résumé, le code testable est-il un meilleur code?
Je ne sais pas, peut-être pas. Les gens ici ont des points valables.
Mais je crois qu'un meilleur code a aussi tendance à être un code testable .
Et que si vous parlez de logiciel sérieux à utiliser dans des projets sérieux, l'envoi de code non testé n'est pas la chose la plus responsable que vous puissiez faire avec l'argent de votre employeur ou de vos clients.
Il est également vrai que certains codes nécessitent des tests plus rigoureux que d’autres et il est un peu ridicule de prétendre le contraire. Comment voudriez-vous avoir été astronaute dans la navette spatiale si le système de menus qui vous interfaçait avec les systèmes vitaux de la navette n'avait pas été testé? Ou un employé d'une centrale nucléaire où les logiciels de surveillance de la température dans le réacteur n'ont pas été testés? D'autre part, un peu de code générant un simple rapport en lecture seule nécessite-t-il un camion porte-conteneur rempli de documentation et un millier de tests unitaires? J'espère bien que non. Juste en disant ...
la source
Vous avez raison, et en vous moquant, vous pouvez rendre le code testable et éviter de passer le temps (jeu de mots intention non définie). Exemple de code:
Maintenant, disons que vous voulez tester ce qui se passe pendant une seconde intercalaire. Comme vous le dites, pour tester cela de manière excessive, il vous faudrait changer le code (de production):
Si Python supportait les secondes intercalaires, le code de test ressemblerait à ceci:
Vous pouvez tester cela, mais le code est plus complexe que nécessaire et les tests ne peuvent toujours pas exercer de manière fiable la branche de code que la plupart des codes de production utiliseront (c'est-à-dire, en ne transmettant pas de valeur
now
). Vous travaillez autour de cela en utilisant une maquette . À partir du code de production d'origine:Code de test:
Cela donne plusieurs avantages:
time_of_day
indépendamment de ses dépendances.Par ailleurs, il est à espérer que les futurs cadres moqueurs faciliteront les choses de ce genre. Par exemple, étant donné que vous devez faire référence à la fonction simulée en tant que chaîne, vous ne pouvez pas forcer facilement les IDE à la modifier automatiquement lorsque vous
time_of_day
commencez à utiliser une autre source temporellement.la source
Une qualité de code bien écrit est qu’il est robuste au changement . C'est-à-dire que lorsqu'un changement d'exigences survient, le changement de code devrait être proportionnel. C'est un idéal (et pas toujours atteint), mais écrire du code testable nous aide à nous rapprocher de cet objectif.
Pourquoi cela nous aide-t-il à nous rapprocher? En production, notre code fonctionne dans l'environnement de production, y compris l'intégration et l'interaction avec tous nos autres codes. Dans les tests unitaires, nous balayons une grande partie de cet environnement. Notre code est maintenant en train de changer, car les tests sont un changement . Nous utilisons les unités de différentes manières, avec différents intrants (faux, mauvais intrants qui pourraient ne jamais être réellement transmis, etc.) à ceux que nous utiliserions dans la production.
Ceci prépare notre code pour le jour où des changements se produiront dans notre système. Supposons que notre calcul de l'heure doit prendre une heure différente en fonction d'un fuseau horaire. Nous avons maintenant la possibilité de passer cette période et de ne plus avoir à modifier le code. Lorsque nous ne voulons pas passer à une heure donnée et que nous voulons utiliser l'heure actuelle, nous pourrions simplement utiliser un argument par défaut. Notre code est robuste au changement car il est testable.
la source
D'après mon expérience, l'une des décisions les plus importantes et les plus ambitieuses que vous preniez lors de la création d'un programme consiste à diviser le code en unités (où "unités" est utilisé dans son sens le plus large). Si vous utilisez un langage OO basé sur les classes, vous devez décomposer tous les mécanismes internes utilisés pour implémenter le programme en un certain nombre de classes. Ensuite, vous devez diviser le code de chaque classe en un certain nombre de méthodes. Dans certaines langues, le choix consiste à diviser votre code en fonctions. Ou, si vous faites de la SOA, vous devez décider du nombre de services que vous allez créer et du contenu de chaque service.
La répartition que vous choisissez a un effet énorme sur l'ensemble du processus. Les bons choix facilitent l’écriture du code et réduisent le nombre de bogues (avant même de commencer les tests et le débogage). Ils facilitent le changement et la maintenance. Fait intéressant, il s’avère qu’une fois que vous avez trouvé une bonne ventilation, il est généralement plus facile à tester qu’une mauvaise.
Pourquoi cela est-il ainsi? Je ne pense pas pouvoir comprendre et expliquer toutes les raisons. Mais une des raisons est qu’une bonne ventilation implique invariablement de choisir une "taille de grain" modérée pour les unités de mise en œuvre. Vous ne voulez pas entasser trop de fonctionnalités et de logique dans une seule classe / méthode / fonction / module / etc. Cela rend votre code plus facile à lire et à écrire, mais cela facilite également le test.
Ce n'est pas que ça, cependant. Une bonne conception interne signifie que le comportement attendu (entrées / sorties / etc.) de chaque unité de mise en œuvre peut être défini de manière claire et précise. Ceci est important pour les tests. Une bonne conception signifie généralement que chaque unité de mise en œuvre aura un nombre modéré de dépendances les unes par rapport aux autres. Cela facilite la lecture et la compréhension de votre code par les autres utilisateurs, mais facilite également le test. Les raisons continuent; peut-être que d'autres peuvent expliquer plus de raisons que je ne peux pas.
En ce qui concerne l'exemple de votre question, je ne pense pas qu'un "bon code" équivaut à affirmer que toutes les dépendances externes (telles qu'une dépendance sur l'horloge système) doivent toujours être "injectées". Cela pourrait être une bonne idée, mais il s’agit d’une question distincte de ce que je décris ici et je n’aborderai pas le pour et le contre.
Incidemment, même si vous appelez directement les fonctions système qui renvoient l'heure actuelle, agissez sur le système de fichiers, etc., cela ne signifie pas que vous ne pouvez pas tester votre code par unité de manière isolée. L'astuce consiste à utiliser une version spéciale des bibliothèques standard qui vous permet de simuler les valeurs de retour des fonctions système. Je n'ai jamais vu d'autres parler de cette technique, mais c'est assez simple à faire avec de nombreux langages et plateformes de développement. (Nous espérons que votre environnement linguistique est open-source et facile à construire. Si votre code implique une étape de lien, il est également facile de contrôler les bibliothèques avec lesquelles il est lié.)
En résumé, un code testable n'est pas nécessairement un "bon" code, mais un "bon" code est généralement testable.
la source
Si vous utilisez les principes SOLID , vous serez du bon côté, surtout si vous prolongez cela avec KISS , DRY et YAGNI .
Un point manquant pour moi est le point de la complexité d'une méthode. Est-ce une simple méthode getter / setter? Ensuite, écrire des tests pour satisfaire votre cadre de tests serait une perte de temps.
S'il s'agit d'une méthode plus complexe dans laquelle vous manipulez des données et que vous voulez être sûr que cela fonctionnera même si vous devez modifier la logique interne, ce serait un excellent appel pour une méthode de test. Plusieurs fois, j'ai dû changer un morceau de code après plusieurs jours / semaines / mois et j'étais vraiment heureux d'avoir le scénario de test. Lors du développement de la méthode, je l’ai testée avec la méthode d’essai, et j’étais certaine que cela fonctionnerait. Après le changement, mon code de test principal fonctionnait toujours. J'étais donc certain que ma modification ne cassait pas un ancien code en production.
Un autre aspect de l'écriture de tests est de montrer aux autres développeurs comment utiliser votre méthode. Plusieurs fois, un développeur cherchera un exemple sur la façon d'utiliser une méthode et quelle sera la valeur de retour.
Juste mes deux cents .
la source