web-dev-qa-db-fra.com

Le code testable est-il un meilleur code?

J'essaie de prendre l'habitude d'écrire régulièrement des tests unitaires avec mon code, mais j'ai lu que d'abord il est important d'écrire du code testable . Cette question touche à SOLID principes d'écriture de code testable, mais je veux savoir si ces principes de conception sont bénéfiques (ou du moins pas nuisibles) sans planifier d'écrire des tests Pour clarifier - je comprends l'importance d'écrire des tests, ce n'est pas une question sur leur utilité.

Pour illustrer ma confusion, dans la pièce 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 le signale comme un mauvais code 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 en argument. À un moment donné, la valeur doit être initialisée, et pourquoi pas la plus proche de la consommation? De plus, le but de la méthode dans mon esprit est de renvoyer une valeur basée sur le heure actuelle, en en faisant un paramètre, vous impliquez que ce but peut/devrait être changé. Ceci et d'autres questions m'amènent à me demander si le code testable était synonyme de "meilleur" code.

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


Le code testable est-il réellement plus stable? a été suggéré comme doublon. Cependant, cette question concerne la "stabilité" du code, mais je demande plus largement si le code est également supérieur pour d'autres raisons, telles que la lisibilité, les performances, le couplage, etc.

103
WannabeCoder

En ce qui concerne la définition commune des tests unitaires, je dirais non. J'ai vu du code simple compliqué en raison de la nécessité de le tordre pour l'adapter au cadre de test (par exemple, les interfaces et IoC partout rendant les choses difficiles à suivre à travers des couches d'appels d'interface et de données qui devraient être évidentes passé par magie). Étant donné le choix entre un code facile à comprendre ou un code facile à tester, je choisis chaque fois le code maintenable.

Cela ne signifie pas de ne pas tester, mais d'adapter les outils qui vous conviennent, et non l'inverse. Il existe d'autres façons 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, Martin Fowler l'attitude d'une unité est généralement une classe, pas une méthode), ou vous pouvez lancer votre programme avec des tests d'intégration automatisés au lieu. Ce n'est peut-être pas aussi joli que votre cadre de test s'allume avec des graduations vertes, mais nous recherchons le code testé, pas la gamification du processus, non?

Vous pouvez rendre votre code facile à maintenir et être toujours bon pour les tests unitaires en définissant de bonnes interfaces entre eux, puis en écrivant des tests qui exercent l'interface publique du composant; ou vous pourriez obtenir un meilleur cadre de test (celui qui remplace les fonctions au moment de l'exécution pour les simuler, plutôt que d'exiger que le code soit compilé avec des simulations en place). Un meilleur cadre 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 juste pour l'adapter à l'outil de test.

118
gbjbaanb

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

Tout d'abord, l'absence de tests est un problème bien plus important que votre code testable ou non. Ne pas avoir de tests unitaires signifie que vous n'avez pas terminé avec votre code/fonctionnalité.

Cela à l'écart, je ne dirais pas qu'il est important d'écrire du code testable - il est important d'écrire du code flexible . Le code inflexible est difficile à tester, il y a donc beaucoup de chevauchement dans l'approche et ce que les gens appellent.

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. Rendez-le flexible - si le code n'est pas flexible, il cessera de fonctionner lorsque les affaires arriveront inévitablement et demandera si le code peut faire XYZ.
  4. Faites vite - au-delà d'un niveau de base acceptable, les performances ne sont que de la sauce.

Les tests unitaires aident à maintenir 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 testable" est généralement un code flexible, ce qui est bon, mais pas aussi important que la fonctionnalité ou la maintenabilité. Pour quelque chose comme l'heure actuelle, rendre cela flexible est bon, mais cela nuit à la maintenabilité en rendant le code plus difficile à utiliser correctement et plus complexe. Étant donné que la maintenabilité est plus importante, je préfère généralement l'approche plus simple même si elle est moins testable.

69
Telastyn

Oui, c'est une bonne pratique. La raison en est que la testabilité n'est pas pour le plaisir des tests. C'est dans un souci de clarté et de compréhensibilité qu'il apporte.

Personne ne se soucie des tests eux-mêmes. C'est une triste réalité de la vie que nous avons besoin de grandes suites de tests de régression parce que nous ne sommes pas assez brillants pour écrire du code parfait sans vérifier constamment nos bases. Si nous le pouvions, le concept de tests serait inconnu, et tout cela ne serait pas un problème. Je souhaite certainement que je pourrais. Mais l'expérience a montré que presque nous tous ne pouvons pas, donc les tests couvrant notre code sont une bonne chose même s'ils prennent du temps à écrire du code d'entreprise.

Comment le fait d'avoir des tests améliore-t-il notre code métier indépendamment du test lui-même? En nous forçant à segmenter nos fonctionnalités en unités dont il est facile de démontrer qu'elles sont correctes. Ces unités sont également plus faciles à obtenir correctement que celles que nous serions autrement tentés d'écrire.

Votre exemple de temps est un bon point. Tant que vous seulement avez une fonction renvoyant l'heure actuelle, vous pourriez penser qu'il est inutile de la programmer. Comment peut-il être difficile de faire les choses correctement? Mais inévitablement, votre programme utilisera cette fonction dans un autre code, et vous voulez certainement tester que code sous différentes conditions, y compris à des moments différents. Par conséquent, c'est une bonne idée de pouvoir manipuler l'heure de retour de votre fonction - non pas parce que vous vous méfiez de votre appel à une ligne currentMillis(), mais parce que vous devez vérifier le appelants de cet appel dans des circonstances contrôlées. Donc, vous voyez, avoir du code testable est utile même si en soi, cela ne semble pas mériter autant d'attention.

51
Kilian Foth

À un moment donné, la valeur doit être initialisée, et pourquoi pas la plus proche 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 garantit que vous pouvez générer ces valeurs en fonction de l'heure que vous souhaitez, et pas seulement "maintenant" (avec "maintenant" signifiant lorsque vous appelez le code).

Rendre le code testable en effet signifie rendre le code qui peut (dès le départ) ê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, il y a un grand avantage à écrire du code réutilisable, et les deux sont synonymes.

De plus, le but de la méthode dans mon esprit est de renvoyer une valeur basée sur l'heure actuelle, en en faisant un paramètre, vous impliquez que ce but peut/devrait être changé.

Vous pouvez également faire valoir que le but de cette méthode est de renvoyer une valeur basée sur une valeur de temps, et vous en avez besoin pour générer celle basée sur "maintenant". L'un d'eux est plus flexible et si vous vous habituez à choisir cette variante, avec le temps, votre taux de réutilisation du code augmentera.

12
utnapistim

Cela peut sembler idiot de le dire de cette façon, mais si vous voulez pouvoir tester votre code, alors oui, il est préférable d'écrire du code testable. Tu demandes:

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

Précisément parce que, dans l'exemple auquel vous faites référence, cela rend ce code non testable. Sauf si vous exécutez uniquement 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. Tout cela est pire que de simplement rendre votre code flexible.

En plus d'être inflexible, cette petite méthode en question a deux responsabilités: (1) obtenir l'heure du système, puis (2) renvoyer 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 logique de répartir davantage les responsabilités afin que la partie hors de votre contrôle (DateTime.Now) a le moins d'impact sur le reste de votre code. Cela rendra le code ci-dessus plus simple, avec pour effet secondaire d'être systématiquement testable.

10
Eric King

Cela a certainement un coût, mais certains développeurs sont tellement habitués à le payer qu'ils ont oublié que le coût est là. Pour votre exemple, vous avez maintenant deux unités au lieu d'une, vous avez besoin du code appelant pour initialiser et gérer une dépendance supplémentaire, et tandis que GetTimeOfDay est plus testable, vous êtes de retour dans le même bateau testant votre nouveau IDateTimeProvider. C'est juste que si vous avez de bons tests, les avantages l'emportent généralement sur les coûts.

De plus, dans une certaine mesure, l'écriture de code testable vous encourage à concevoir votre code de manière plus maintenable. Le nouveau code de gestion des dépendances est ennuyeux, vous voudrez donc regrouper toutes vos fonctions dépendantes du temps, si possible. Cela peut aider à atténuer et à corriger les bogues comme, par exemple, lorsque vous chargez une page directement sur une limite de temps, certains éléments étant rendus en utilisant l'heure avant et certains en utilisant l'heure après. Il peut également accélérer votre programme en évitant les appels système répétés pour obtenir l'heure actuelle.

Bien sûr, ces améliorations architecturales dépendent fortement de quelqu'un qui remarque les opportunités et les met en œuvre. L'un des plus grands dangers de se concentrer si étroitement sur les unités est de perdre de vue la situation dans son ensemble.

De nombreux frameworks de tests unitaires vous permettent de patcher un objet simulé lors de l'exécution, ce qui vous permet de profiter des avantages de la testabilité sans tous les dégâts. Je l'ai même vu faire en C++. Examinez cette capacité dans des situations où il semble que le coût de testabilité n'en vaut pas la peine.

9
Karl Bielefeldt

Il est possible que toutes les caractéristiques qui contribuent à la testabilité ne soient pas souhaitables en dehors du contexte de la 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 de manière générale les caractéristiques qui contribuent à 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 à être réutilisés. Il est bien organisé et bien nommé (pour pouvoir tester certaines fonctionnalités, vous accordez plus d'attention à la dénomination; si vous n'écriviez pas de tests, 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 initialement prévu. C'est SEC, donc moins encombré et plus facile à comprendre.

Oui. Il est recommandé d'écrire du code testable, même indépendamment des tests.

8
Carl Manaster

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 qui est juste odieuse à gérer, ne peut pratiquement pas être touchée sans casser un ou plusieurs obscurs, non dépendances évidentes ailleurs (ou quelque part en lui-même, où la dépendance est presque impossible à visualiser) et est à peu près par définition non testable. L'idée (qui n'est pas sans fondement) que les frameworks de test sont devenus exagérés ne devrait pas être considérée comme une licence gratuite pour écrire du code de mauvaise qualité et non testable, à mon avis.

Les idéaux de développement pilotés par les tests ont tendance à vous pousser à écrire des procédures à responsabilité unique, par exemple, et que est certainement une bonne chose. Personnellement, je dis acheter une responsabilité unique, une source unique de vérité, une portée contrôlée (pas de variables globales bizarres) et garder les dépendances fragiles au minimum, et votre code sera testable. Testable par un cadre de test spécifique? Qui sait. Mais c'est peut-être le cadre de test qui doit s'adapter au bon code, et non l'inverse.

Mais juste pour être clair, un code si intelligent, ou si long et/ou interdépendant qu'il n'est pas facilement compréhensible par un autre être humain n'est pas un bon code. Et ce n'est pas non plus, par coïncidence, du code qui peut être facilement testé.

Alors, à l'approche de mon résumé, testable code better code?

Je ne sais pas, peut-être pas. Les gens ici ont des points valables.

Mais je crois que mieux le code a tendance à être aussi testable code.

Et que si vous parlez de logiciels sérieux à utiliser dans des efforts 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 codes et il est un peu idiot de prétendre le contraire. Comment auriez-vous aimé être astronaute sur 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 systèmes logiciels de surveillance de la température dans le réacteur n'ont pas été testés? D'un autre côté, un peu de code générant un simple rapport en lecture seule nécessite-t-il un camion porte-conteneurs rempli de documentation et de mille tests unitaires? J'espère bien que non. Je dis juste ...

8
Craig

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

Vous avez raison, et avec la moquerie, vous pouvez rendre le code testable et éviter de passer le temps (intention de jeu de mots non définie). Exemple de code:

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

Supposons maintenant que vous souhaitiez tester ce qui se passe pendant une seconde intercalaire. Comme vous le dites, pour tester cela de manière excessive, vous devrez 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 prise en charge 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 le tester, mais le code est plus complexe que nécessaire et les tests encore ne peuvent pas exercer de manière fiable la branche de code que la plupart des codes de production utiliseront (c'est-à-dire ne pas transmettre de valeur pour now). Pour contourner ce problème, utilisez un 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 comme code de production.
  • Le code de production est aussi simple que possible.

Soit dit en passant, il faut espérer que les futurs cadres de simulation rendront les choses de ce genre plus faciles. Par exemple, puisque vous devez faire référence à la fonction simulée comme une chaîne, vous ne pouvez pas facilement faire en sorte que les IDE la modifient automatiquement lorsque time_of_day commence à utiliser une autre source de temps.

5
l0b0

Une qualité de code bien écrit est qu'il est robuste pour changer. Autrement dit, lorsqu'un changement d'exigences survient, le changement dans le code doit être proportionnel. C'est un idéal (et pas toujours atteint), mais l'écriture de 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 en intégrant et en interagissant avec tous nos autres codes. Dans les tests unitaires, nous balayons une grande partie de cet environnement. Notre code est maintenant robuste pour changer parce que les tests sont un changement . Nous utilisons les unités de différentes manières, avec des entrées différentes (simulations, mauvaises entrées qui pourraient ne jamais être transmises, etc.) que nous les utiliserions en production.

Cela prépare notre code pour le jour où le changement se produit dans notre système. Disons que notre calcul du temps doit prendre un temps différent en fonction d'un fuseau horaire. Maintenant, nous avons la possibilité de passer ce temps et de ne pas avoir à modifier le code. Lorsque nous ne voulons pas passer de temps et que nous voulons utiliser l'heure actuelle, nous pouvons simplement utiliser un argument par défaut. Notre code est robuste pour changer car il est testable.

4
cbojar

D'après mon expérience, l'une des décisions les plus importantes et les plus profondes que vous prenez lors de la construction d'un programme est la façon dont vous divisez le code en unités (où "unités" est utilisé dans son sens le plus large). Si vous utilisez un langage basé sur les classes OO, vous devez diviser 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 nombre de méthodes. Dans certains langages, le choix est de savoir comment diviser votre code en fonctions. Ou si vous faites la chose SOA, vous devez décider combien de services vous allez construire et ce qui ira dans chaque service.

La ventilation que vous choisissez a un effet énorme sur l'ensemble du processus. De bons choix facilitent l'écriture du code et entraînent moins de bogues (avant même de commencer les tests et le débogage). Ils facilitent le changement et l'entretien. Fait intéressant, il s'avère qu'une fois que vous avez trouvé une bonne ventilation, il est généralement plus facile de tester qu'une mauvaise.

Pourquoi cela est-il ainsi? Je ne pense pas pouvoir comprendre et expliquer toutes les raisons. Mais l'une des raisons est qu'une bonne répartition signifie invariablement le choix d'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 trop de logique dans une seule classe/méthode/fonction/module/etc. Cela rend votre code plus facile à lire et à écrire, mais il est également plus facile à tester.

Ce n'est pas seulement ç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é d'implémentation aura un nombre modéré de dépendances sur les autres. Cela rend votre code plus facile à lire et à comprendre pour les autres, mais facilite également les tests. Les raisons continuent; peut-être que d'autres peuvent exprimer plus de raisons que je ne peux pas.

En ce qui concerne l'exemple de votre question, je ne pense pas qu'une "bonne conception de code" équivaut à dire que toutes les dépendances externes (comme une dépendance à l'horloge système) doivent toujours être "injectées". C'est peut-être une bonne idée, mais c'est un problème distinct de ce que je décris ici et je ne vais pas me plonger dans ses avantages et ses inconvénients.

Soit dit en passant, même si vous appelez directement des fonctions système qui renvoient l'heure actuelle, agissent sur le système de fichiers, etc., cela ne signifie pas que vous ne pouvez pas tester votre code 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 mentionner cette technique, mais c'est assez simple à faire avec de nombreux langages et plateformes de développement. (Espérons que votre runtime de langage soit open-source et facile à construire. Si l'exécution de votre code implique une étape de lien, nous espérons qu'il sera également facile de contrôler les bibliothèques avec lesquelles il est lié.)

En résumé, le code testable n'est pas nécessairement un "bon" code, mais un "bon" code est généralement testable.

4
Alex D

Si vous optez pour les principes SOLIDES , vous serez du bon côté, surtout si vous étendez cela avec KISS , SEC , 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? Il suffirait alors d'écrire des tests pour satisfaire votre cadre de test.

Si c'est une méthode plus complexe où vous manipulez des données et voulez être sûr que cela fonctionnera même si vous devez changer la logique interne, alors ce serait un excellent appel à 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 cas de test. Lors du premier développement de la méthode, je l'ai testée avec la méthode de test, et j'étais sûr que cela fonctionnerait. Après le changement, mon code de test principal fonctionnait toujours. J'étais donc certain que mon changement n'avait pas cassé un vieux 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 recherchera un exemple sur la façon d'utiliser une méthode et quelle sera la valeur de retour.

Juste mon deux cents .

1
BtD