Le code testable est-il un meilleur code?

103

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.

WannabeCoder
la source
24
Il existe une propriété spéciale de la fonction qui vous oblige à passer le temps appelé idempotency. Une telle fonction produira le même résultat chaque fois qu'elle sera appelée avec une valeur d'argument donnée, ce qui non seulement la rend plus testable, mais également plus composable et plus facile à raisonner.
Robert Harvey
4
Pouvez-vous définir "meilleur code"? voulez-vous dire "maintenable" ?, "plus facile à utiliser sans IOC-Container-Magic"?
k3b
7
Je suppose que les tests n'ont jamais échoué car ils utilisaient l'heure système réelle, puis le décalage de fuseau horaire a été modifié.
Andy
5
C'est mieux que du code non testable.
Tulains Córdova
14
@RobertHarvey Je n'appellerais pas cela idempotency, je dirais que c'est une transparence référentielle : en cas de func(X)retour "Morning", le remplacement de toutes les occurrences de func(X)with "Morning"ne changera pas le programme (c.-à-d. Que l'appel funcne fait rien que renvoyer la valeur). Idempotency implique soit ça func(func(X)) == X(ce qui n’est pas le type correct), soit cela func(X); func(X);produit les mêmes effets secondaires que func(X)(mais il n’ya pas d’effets secondaires ici)
Warbo

Réponses:

116

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.

gbjbaanb
la source
3
Les commentaires ne sont pas pour une discussion prolongée; cette conversation a été déplacée pour discuter .
Ingénieur mondial
2
Je pense que cela vaut la peine de noter que je connais au moins un langage qui vous permet de faire ce que décrit votre dernier paragraphe: Python avec Mock. En raison de la manière dont les importations de modules fonctionnent, pratiquement n'importe quoi, mis à part les mots clés, peut être remplacé par une maquette, même les méthodes / classes / etc de l'API standard. C'est donc possible, mais cela pourrait nécessiter que le langage soit conçu de manière à permettre une telle flexibilité.
jpmc26
6
Je pense qu'il y a une différence entre "code testable" et "code [tordu] pour s'adapter au framework de test". Je ne sais pas trop où je vais avec ce commentaire, si ce n'est que je conviens que le code "tordu" est mauvais et que le code "testable" avec de bonnes interfaces est bon.
Bryan Oakley
2
J'ai exprimé certaines de mes pensées dans les commentaires de l' article (puisque les commentaires étendus ne sont pas autorisés ici), jetez-y un coup d'œil! Pour être clair: je suis l'auteur de l'article mentionné :)
Sergey Kolodiy
Je suis d'accord avec BryanOakley. "Code testable" suggère que vos préoccupations sont séparées: il est possible de tester un aspect (module) sans interférence provenant d'autres aspects. Je dirais que cela est différent de "ajuster les conventions de test spécifiques à votre support de projet". Ceci est similaire aux modèles de conception: ils ne doivent pas être forcés. Un code qui utilise correctement les modèles de conception serait considéré comme un code fort. Même chose pour les principes de test. Si rendre votre code "testable" entraîne une torsion excessive du code de votre projet, vous faites quelque chose de mal.
Vince Emigh
68

L'écriture de code testable reste-t-elle une bonne pratique même en l'absence de tests?

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:

  1. Faites-le fonctionner - si le code ne fait pas ce qu'il doit faire, il ne vaut rien.
  2. Rendez-le maintenable - si le code n'est pas maintenable, il cessera rapidement de fonctionner.
  3. Faites preuve de souplesse - si le code n'est pas flexible, il cessera de fonctionner lorsque les affaires arriveront inévitablement et vous demanderont si le code peut fonctionner avec XYZ.
  4. Faites vite - au-delà d’un niveau de base acceptable, la performance n’est que de la sauce.

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.

Telastyn
la source
4
J'aime la relation que vous indiquez entre testable et flexible, ce qui rend le problème plus compréhensible pour moi. La flexibilité permet à votre code de s’adapter, mais le rend nécessairement un peu plus abstrait et moins intuitif à comprendre, mais c’est un sacrifice louable pour les avantages.
WannabeCoder
3
Cela dit, je vois souvent des méthodes qui auraient dû être privées être forcées au niveau public ou au niveau du package afin que le cadre de test unitaire puisse y accéder directement. Loin d'une approche idéale.
Jwenting
4
@WannabeCoder Bien sûr, cela ne vaut la peine d'ajouter de la flexibilité que si vous gagnez du temps. C'est pourquoi nous n'écrivons pas chaque méthode par rapport à une interface. La plupart du temps, il est simplement plus facile de réécrire le code plutôt que d'incorporer trop de flexibilité dès le départ. YAGNI est toujours un principe extrêmement puissant - assurez-vous simplement que, quelle que soit la chose "vous n’allez pas avoir besoin", son ajout rétroactif ne vous demandera pas plus de travail en moyenne que de le mettre en œuvre à l’avance. C'est le code qui ne suit pas YAGNI qui pose le plus de problèmes de flexibilité dans mon expérience.
Luaan
3
"Ne pas avoir de tests unitaires signifie que vous n'avez pas fini avec votre code / fonctionnalité" - Faux. La "définition du fait" est quelque chose que l'équipe décide. Cela peut inclure ou non un certain degré de couverture du test. Mais nulle part il n’existe une exigence stricte selon laquelle une fonctionnalité ne peut pas être "réalisée" s’il n’existe aucun test. L’équipe peut choisir d’exiger des tests ou pas.
aroth
3
@Telastyn Au cours de plus de 10 années de développement, aucune équipe n’a imposé un cadre de test unitaire, et deux seulement en ont même eu un (la couverture était médiocre). Un endroit nécessitait un document expliquant comment tester la fonctionnalité que vous écriviez. C'est ça. Peut-être que je suis malchanceux? Je ne suis pas un test anti-unité (sérieusement, j'ai modifié le site SQA.SE, je suis un test unitaire très pro!), Mais je ne l'ai pas trouvé aussi répandu que ce que votre déclaration affirme.
CorsiKa
50

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.

Kilian Foth
la source
Un autre exemple est si vous souhaitez extraire une partie du code d'un projet à un autre endroit (pour une raison quelconque). Plus les différentes parties de la fonctionnalité sont indépendantes les unes des autres, plus il est facile d’extraire exactement la fonctionnalité dont vous avez besoin et rien de plus.
valenterry
10
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.
jcollum
Je lis lentement sur les pratiques de test depuis un certain temps maintenant (en quelque sorte qui ne fait pas encore de test d'unité) et je dois dire que la dernière partie concerne la vérification de l'appel dans des circonstances contrôlées et le code plus souple qui accompagne il a fait toutes sortes de choses en place. Je vous remercie.
plast1k
12

À un moment donné, la valeur doit être initialisée et pourquoi pas au plus près de la consommation?

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.

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é.

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.

utnapistim
la source
10

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:

À un moment donné, la valeur doit être initialisée et pourquoi pas au plus près de la consommation?

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.

public static string GetTimeOfDay()
{
    DateTime time = DateTime.Now;
    if (time.Hour >= 0 && time.Hour < 6)
    {
        return "Night";
    }
    if (time.Hour >= 6 && time.Hour < 12)
    {
        return "Morning";
    }
    if (time.Hour >= 12 && time.Hour < 18)
    {
        return "Afternoon";
    }
    return "Evening";
}

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é.

Eric King
la source
1
Vous devrez donc effectuer des tests tôt le matin pour vérifier que vous obtenez le résultat "Nuit" quand vous le souhaitez. C'est difficile. Supposons maintenant que vous souhaitiez vérifier que le traitement des dates est correct le 29 février 2016 ... Et certains programmeurs iOS (et probablement d'autres) sont en proie à une erreur du débutant tester pour cela. Et par expérience, je vérifierais le traitement des données le 2 février 2020.
gnasher729
1
@ gnasher729 Exactement mon point. "Rendre ce code testable" est un changement simple qui peut résoudre beaucoup de problèmes (de test). Si vous ne voulez pas automatiser les tests, alors je suppose que le code est passable tel quel. Mais ce serait mieux une fois "testable".
Eric King
9

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 GetTimeOfDaysoit plus testable, vous êtes de retour dans le même bateau pour tester votre nouvelle IDateTimeProvider. 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.

Karl Bielefeldt
la source
+1: vous devez améliorer la conception et l'architecture pour faciliter la rédaction de tests unitaires.
BЈовић
3
+ - c'est l'architecture de votre code qui compte. Des tests plus faciles ne sont qu'un heureux effet secondaire.
gbjbaanb
8

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.

Carl Manaster
la source
désaccord sur le fait que DRY - encapsuler GetCurrentTime dans une méthode MyGetCurrentTime répète en grande partie l’appel du système d’exploitation sans aucun avantage, si ce n’est pour aider l’outil de test. Ce n'est que le plus simple des exemples, ils empirent beaucoup en réalité.
gbjbaanb
1
"Répéter l'appel du système d'exploitation sans aucun avantage" - jusqu'à ce que vous finissiez par exécuter sur un serveur avec une horloge, parler à un serveur aws dans un fuseau horaire différent, et cela rompt votre code, et vous devez alors parcourir tout votre code et mettez-le à jour pour utiliser MyGetCurrentTime, qui renvoie à la place UTC. ; horloge oblique, heure avancée, et il y a d'autres raisons pour lesquelles ce ne serait peut-être pas une bonne idée de faire une confiance aveugle à l'appel du système d'exploitation, ou au moins d'avoir un seul point où vous pouvez changer de remplaçant.
Andrew Hill
8

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 ...

Craig
la source
1
"le meilleur code a tendance à être aussi du code testable" C'est la clé. Le rendre testable ne le rend pas meilleur. L'améliorer rend souvent le testable, et les tests vous donnent souvent des informations que vous pouvez utiliser pour l'améliorer, mais la simple présence de tests n'implique pas la qualité et il existe de rares exceptions.
Anaximandre le
1
Exactement. Considérez le contrapositive. S'il s'agit d'un code non testable, il n'est pas testé. Si ce n'est pas testé, comment savez-vous si cela fonctionne ou non autrement qu'en situation réelle?
pjc50
1
Tous les tests prouvent que le code réussit les tests. Sinon, le code testé par l'unité serait sans bug et nous savons que ce n'est pas le cas.
wobbily_col
@ anaximander Exactement. Il existe au moins la possibilité que la simple présence de tests soit une contre-indication qui entraîne une qualité de code plus médiocre si le seul objectif est de cocher les cases à cocher. "Au moins sept tests unitaires pour chaque fonction?" "Vérifier." Mais je crois vraiment que si le code est un code de qualité, il sera plus facile à tester.
Craig
1
... mais faire d'une bureaucratie une partie des tests peut être un gaspillage total et ne pas produire d'informations utiles ou de résultats fiables. Indépendamment; J'aimerais vraiment que quelqu'un ait testé le bug SSL Heartbleed , ouais? ou le bug Apple goto fail ?
Craig
5

Pour moi, cependant, il semble exagéré de passer le temps comme argument.

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:

def time_of_day():
    return datetime.datetime.utcnow().strftime('%H:%M:%S')

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

def time_of_day(now=None):
    now = now if now is not None else datetime.datetime.utcnow()
    return now.strftime('%H:%M:%S')

Si Python supportait les secondes intercalaires, le code de test ressemblerait à ceci:

def test_handle_leap_second(self):
    actual = time_of_day(
        now=datetime.datetime(year=2015, month=6, day=30, hour=23, minute=59, second=60)
    expected = '23:59:60'
    self.assertEquals(actual, expected)

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:

def time_of_day():
    return datetime.datetime.utcnow().strftime('%H:%M:%S')

Code de test:

@unittest.patch('datetime.datetime.utcnow')
def test_handle_leap_second(self, utcnow_mock):
    utcnow_mock.return_value = datetime.datetime(
        year=2015, month=6, day=30, hour=23, minute=59, second=60)
    actual = time_of_day()
    expected = '23:59:60'
    self.assertEquals(actual, expected)

Cela donne plusieurs avantages:

  • Vous testez time_of_day indépendamment de ses dépendances.
  • Vous testez le même chemin de code que le code de production.
  • Le code de production est aussi simple que possible.

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_daycommencez à utiliser une autre source temporellement.

l0b0
la source
FYI: votre argument par défaut est faux. Elle ne sera définie qu'une seule fois, votre fonction retournera donc toujours l'heure à laquelle elle a été évaluée.
ahruss
4

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.

cbojar
la source
4

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.

Alex D
la source
1

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 .

BtD
la source