TDD: une méthodologie de développement qui a changé ma vie

À 7 h 15 Notre support technique est inondé de travail. Good Morning America vient de parler de nous et beaucoup de ceux qui visitent notre site pour la première fois ont rencontré des erreurs.

Nous avons une vraie ruée. En ce moment, avant de perdre l'occasion de transformer les visiteurs de la ressource en nouveaux utilisateurs, nous allons déployer le groupe de correctifs. L'un des développeurs a préparé quelque chose. Il pense que cela aidera à faire face au problème. Nous plaçons un lien vers la version mise à jour du programme, qui n'est pas encore en production, vers le chat de l'entreprise, et nous demandons à tout le monde de le tester. Ça marche!

Nos ingénieurs héroïques exécutent des scripts pour déployer les systèmes et après quelques minutes, la mise à jour entre en bataille. Du coup, le nombre d'appels au support technique double. Notre solution urgente a cassé quelque chose, les développeurs ont attrapé le blâme et les ingénieurs ont restauré le système à son état précédent à ce moment-là.

image

L'auteur du document, dont nous publions aujourd'hui la traduction, estime que tout cela aurait pu être évité grâce à TDD.

Pourquoi est-ce que j'utilise TDD?


Je ne suis pas allé dans de telles situations depuis longtemps. Et ce n'est pas que les développeurs ont cessé de faire des erreurs. Le fait est que depuis de nombreuses années maintenant, dans chaque équipe que j'ai dirigée et influencée, la méthodologie TDD a été appliquée. Bien sûr, des erreurs se produisent toujours, mais la pénétration dans la production de problèmes qui peuvent «faire tomber» le projet est tombée à presque zéro, même si la fréquence des mises à jour logicielles et le nombre de tâches à résoudre pendant la mise à jour ont augmenté de façon exponentielle depuis lors. quand quelque chose est arrivé dont j'ai parlé au début.

Quand quelqu'un me demande pourquoi il devrait contacter TDD, je lui raconte cette histoire et je me souviens d'une douzaine d'autres cas similaires. L'une des raisons les plus importantes pour lesquelles je suis passé au TDD est que cette méthodologie améliore la couverture des tests avec du code, ce qui conduit à 40 à 80% d'erreurs de production en moins . C'est ce que j'aime le plus chez TDD. Cela enlève une montagne de problèmes des épaules des développeurs.

De plus, il convient de noter que TDD évite aux développeurs de craindre d'apporter des modifications au code.

Dans les projets auxquels je participe, des ensembles de modules automatiques et de tests fonctionnels empêchent presque quotidiennement la mise en production du code, ce qui peut sérieusement perturber le travail de ces projets. Par exemple, maintenant je regarde 10 mises à jour de bibliothèque automatiques effectuées la semaine dernière, comme avant de les publier sans utiliser TDD, je crains qu'elles ne ruinent quelque chose.

Toutes ces mises à jour ont été automatiquement intégrées au code et sont déjà utilisées en production. Je n'ai vérifié aucun d'entre eux manuellement et je ne craignais pas du tout qu'ils puissent avoir un mauvais effet sur le projet. En même temps, pour donner cet exemple, je n'ai pas eu à réfléchir longtemps. Je viens d'ouvrir GitHub, j'ai regardé les récentes fusions et j'ai vu de quoi je parlais. La tâche qui était précédemment résolue manuellement (ou, pire encore, le problème qui a été ignoré) est maintenant un processus d'arrière-plan automatisé. Vous pouvez essayer de faire quelque chose de similaire sans une bonne couverture de code avec des tests, mais je ne recommanderais pas de le faire.

Qu'est-ce que TDD?


TDD signifie Test Driven Development. Le processus mis en œuvre en appliquant cette méthodologie est très simple:


Les tests détectent les erreurs, les tests réussissent, le refactoring est effectué

Voici les principes de base de l'utilisation de TDD:

  1. Avant d'écrire un code d'implémentation pour une fonctionnalité, ils écrivent un test qui vous permet de vérifier si ce futur code d'implémentation fonctionne ou non. Avant de passer à l'étape suivante, le test est lancé et convaincu qu'il génère une erreur. Grâce à cela, vous pouvez être sûr que le test ne produit pas de faux positifs, c'est une sorte de test des tests eux-mêmes.
  2. Ils créent une implémentation de l'opportunité et s'assurent qu'elle réussit le test.
  3. Effectuez, si nécessaire, une refactorisation de code. La refactorisation, en présence d'un test qui peut indiquer au développeur si le système fonctionne correctement ou incorrectement, donne confiance au développeur dans ses actions.

Comment TDD peut-il aider à gagner du temps pour développer des programmes?


À première vue, il peut sembler que l'écriture de tests signifie une augmentation significative de la quantité de code de projet, et que tout cela prend beaucoup de temps aux développeurs. Dans mon cas, au début, tout n'était que ça, et j'ai essayé de comprendre comment, en principe, il est possible d'écrire le code testé, et comment ajouter des tests au code déjà écrit.

TDD se caractérise par une certaine courbe d'apprentissage, et tandis qu'un débutant grimpe le long de cette courbe, le temps nécessaire au développement peut augmenter de 15 à 35% . C'est souvent exactement ce qui se passe. Mais quelque part environ 2 ans après le début de l'utilisation de TDD, quelque chose d'incroyable commence à se produire. À savoir, par exemple, j'ai commencé, avec l'écriture préliminaire des tests unitaires, à programmer plus rapidement qu'auparavant lorsque TDD n'était pas utilisé.

Il y a quelques années, j'ai implémenté, dans le système client, la possibilité de travailler avec des fragments d'un clip vidéo. À savoir, le fait était qu'il serait possible de permettre à l'utilisateur d'indiquer le début et la fin du fragment d'enregistrement, et de recevoir un lien vers celui-ci, ce qui permettrait de se référer à un endroit spécifique dans le clip, et non à tout ce clip.

Je n'ai pas travaillé. Le joueur a atteint la fin du fragment et a continué à le jouer, mais je ne savais pas pourquoi.

J'ai pensé que le problème était une mauvaise connexion des écouteurs d'événements. Mon code ressemblait à ceci:

video.addEventListener('timeupdate', () => {  if (video.currentTime >= clip.stopTime) {    video.pause();  } }); 

Le processus de recherche du problème ressemblait à ceci: apporter des modifications, compiler, redémarrer, cliquer, attendre ... Cette séquence d'actions se répétait encore et encore.

Afin de vérifier chacun des changements introduits dans le projet, cela a pris près d'une minute à passer, et j'ai expérimenté une infinité d'options pour résoudre le problème (la plupart d'entre elles 2-3 fois).

J'ai peut-être fait une erreur dans le mot clé timeupdate ? Ai-je bien compris les fonctionnalités de travail avec l'API? L'appel video.pause() t-il? J'ai apporté des modifications au code, ajouté console.log() , je suis retourné au navigateur, j'ai cliqué sur le bouton , j'ai cliqué sur la position située à la fin du fragment sélectionné, puis j'ai attendu patiemment que le clip soit entièrement lu. La journalisation à l'intérieur de la construction if n'a conduit à rien. Cela ressemblait à un indice sur un problème possible. J'ai copié le mot timeupdate de la documentation de l'API afin d'être absolument sûr que je n'ai pas fait d'erreur en le saisissant. Je recharge à nouveau la page, cliquez à nouveau, attendez à nouveau. Et encore une fois, le programme refuse de fonctionner correctement.

J'ai finalement mis console.log() dehors du bloc if . «Ça n'aidera pas», ai-je pensé. En fin de compte, l' if était si simple que je ne savais tout simplement pas comment l'orthographier incorrectement. Mais la journalisation dans ce cas a fonctionné. J'ai étouffé avec du café. "Qu'est-ce que c'est que ça!?" Pensai-je.
Loi de débogage de Murphy. L'endroit du programme que vous n'avez jamais testé, puisque vous croyiez fermement qu'il ne pouvait pas contenir d'erreurs, se révélera être exactement l'endroit où vous trouverez une erreur après avoir été complètement épuisé, vous ne modifierez cet endroit que parce que qu'ils ont déjà essayé tout ce à quoi ils pouvaient penser.

J'ai défini un point d'arrêt dans le programme afin de comprendre ce qui se passe. J'ai exploré la signification de clip.stopTime . À ma grande surprise, ce n'était undefined . Pourquoi? J'ai relu le code. Lorsque l'utilisateur sélectionne l'heure de fin du fragment, le programme place le marqueur de fin du fragment au bon endroit, mais ne définit pas la valeur de clip.stopTime . «Je suis un idiot incroyable», pensais-je, «je ne dois pas être autorisé à accéder aux ordinateurs avant la fin de ma vie.»

Je ne l'ai pas oublié et des années plus tard. Et tout - grâce à la sensation que j'ai ressentie, trouvant toujours une erreur. Vous savez probablement de quoi je parle. Avec tout cela est arrivé. Et, peut-être, tout le monde pourra se reconnaître dans ce mème.


Voici à quoi je ressemble quand je programme

Si j'écrivais ce programme aujourd'hui, je commencerais à travailler dessus comme ceci:

 describe('clipReducer/setClipStopTime', async assert => { const stopTime = 5; const clipState = {   startTime: 2,   stopTime: Infinity }; assert({   given: 'clip stop time',   should: 'set clip stop time in state',   actual: clipReducer(clipState, setClipStopTime(stopTime)),   expected: { ...clipState, stopTime } }); }); 

On a l'impression qu'il y a beaucoup plus de code que dans cette ligne:

 clip.stopTime = video.currentTime 

Mais c'est tout. Ce code agit comme une spécification. C'est à la fois une documentation et une preuve que le code fonctionne comme requis par cette documentation. Et, puisque cette documentation existe, si je change l'ordre de travail avec le marqueur pour l'heure de fin d'un fragment, je n'ai pas à me soucier de savoir si lors de l'introduction de ces modifications j'ai violé le bon fonctionnement avec l'heure de fin du clip.

Voici , en passant, du matériel utile pour écrire des tests unitaires, le même que celui que nous venons de voir.

L'important n'est pas le temps qu'il faut pour saisir ce code. Le point est de savoir combien de temps il faut pour déboguer en cas de problème. Si le code est incorrect, le test donnera un excellent rapport d'erreur. Je saurai immédiatement que le problème n'est pas le gestionnaire d'événements. Je saurai que c'est soit dans setClipStopTime() , soit dans clipReducer() , où un changement d'état est implémenté. Grâce au test, je saurais quelles fonctions le code effectue, ce qu'il affiche réellement et ce qu'on attend de lui. Et, plus important encore, mon collègue aura les mêmes connaissances qui, six mois après avoir écrit le code, y introduiront de nouvelles fonctionnalités.

Commençant un nouveau projet, j'ai, comme l'une des premières choses, mis en place un script d'observateur qui exécute automatiquement des tests unitaires chaque fois qu'un certain fichier est modifié. Je programme souvent avec deux moniteurs. Sur l'un d'eux, la console du développeur est ouverte, dans laquelle les résultats d'un tel script sont affichés, de l'autre, l'interface de l'environnement dans lequel j'écris le code est affichée. Lorsque j'apporte une modification au code, en général, en 3 secondes, je vérifie si la modification s'est avérée efficace ou non.

Pour moi, TDD est bien plus qu'une simple assurance. C'est la possibilité de recevoir en permanence et rapidement, en temps réel, des informations sur l'état de mon code. Récompense instantanée sous forme de tests réussis, ou un rapport instantané d'erreurs dans le cas où j'ai fait quelque chose de mal.

Comment la méthodologie TDD m'a-t-elle appris à écrire un meilleur code?


Je voudrais faire une admission, même admettre que c'est embarrassant: je ne savais pas comment créer des applications avant d'apprendre TDD et les tests unitaires. Je ne peux pas imaginer du tout comment j'ai été embauché, mais après avoir interviewé plusieurs centaines de développeurs, je peux dire avec confiance que de nombreux programmeurs se trouvent dans une situation similaire. La méthodologie TDD m'a appris presque tout ce que je sais sur la décomposition et la composition efficaces des composants logiciels (je veux dire les modules, les fonctions, les objets, les composants de l'interface utilisateur, etc.).

La raison en est que les tests unitaires forcent le programmeur à tester les composants séparément les uns des autres et des sous-systèmes d'E / S. Si le module est fourni avec certaines données d'entrée, il doit fournir certaines données de sortie, précédemment connues. S'il ne le fait pas, le test échoue. Si c'est le cas, le test réussit. Le point ici est que le module devrait fonctionner indépendamment du reste de l'application. Si vous testez la logique de l'état, vous devriez pouvoir le faire sans afficher quoi que ce soit à l'écran ni enregistrer quoi que ce soit dans la base de données. Si vous testez la formation de l'interface utilisateur, vous devriez pouvoir la tester sans avoir à charger la page dans un navigateur ou à accéder aux ressources réseau.

Entre autres choses, la méthodologie TDD m'a appris que la vie devient beaucoup plus facile si vous vous efforcez de minimaliser lors du développement de composants d'interface utilisateur. De plus, la logique métier et les effets secondaires doivent être isolés de l'interface utilisateur. D'un point de vue pratique, cela signifie que si vous utilisez un cadre d'interface utilisateur basé sur des composants tel que React ou Angular, il peut être conseillé de créer des composants de présentation qui sont chargés d'afficher quelque chose à l'écran et des composants de conteneurs qui ne sont pas connectés les uns aux autres. sont mélangés.

Un composant de présentation qui reçoit certaines propriétés génère toujours le même résultat. Ces composants peuvent être facilement vérifiés à l'aide de tests unitaires. Cela vous permet de savoir si le composant fonctionne correctement avec les propriétés et si certaines logiques conditionnelles utilisées dans la formation de l'interface sont correctes. Par exemple, il est possible que le composant formant la liste ne présente rien d'autre qu'une invitation à ajouter un nouvel élément à la liste si la liste est vide.

Je connaissais le principe de la séparation des responsabilités bien avant de maîtriser le TDD, mais je ne savais pas comment partager les responsabilités entre différentes entités.

Les tests unitaires m'ont permis d'étudier l'utilisation des mokas pour tester quelque chose, puis j'ai découvert que le moking est un signe que quelque chose ne va pas avec le code . Cela m'a stupéfait et a complètement changé mon approche de la composition logicielle.

Tout développement logiciel est une composition: le processus de décomposition de gros problèmes en plusieurs petits problèmes faciles à résoudre, puis de création de solutions à ces problèmes qui forment l'application. Tuxing pour des tests unitaires indique que les unités atomiques de la composition ne sont, en fait, pas atomiques. L'étude de la façon de se débarrasser de mok sans affecter la couverture du code par des tests m'a permis d'apprendre à identifier les innombrables raisons cachées de la forte connectivité des entités.

Cela m'a permis, en tant que développeur, d'évoluer professionnellement. Cela m'a appris à écrire du code beaucoup plus simple, plus facile à étendre, à maintenir, à mettre à l'échelle. Cela s'applique à la complexité du code lui-même et à l'organisation de son travail dans de grands systèmes distribués comme les infrastructures cloud.

Comment TDD fait-il gagner du temps à l'équipe?


J'ai déjà dit que TDD, en premier lieu, conduit à une meilleure couverture du code avec des tests. La raison en est que nous ne commençons pas à écrire du code pour implémenter une fonctionnalité avant d'avoir écrit un test qui vérifie le bon fonctionnement de ce futur code. Nous écrivons d'abord un test. Ensuite, nous lui permettons de se terminer par une erreur. Ensuite, nous écrivons le code pour implémenter l'opportunité. Nous testons le code, nous recevons un message d'erreur, nous réussissons correctement les tests, nous effectuons le refactoring et répétons ce processus.

Ce processus vous permet de créer une "clôture" à travers laquelle seules quelques erreurs peuvent "sauter". Cette protection contre les erreurs a un effet étonnant sur toute l'équipe de développement. Cela soulage la peur de l'équipe de fusion.

Le haut niveau de couverture du code avec des tests permet à l'équipe de se débarrasser du désir de contrôler manuellement tout, même petit, changement dans la base de code. Les modifications de code deviennent une partie naturelle du flux de travail.

Se débarrasser de la peur d'apporter des modifications au code ressemble au flou d'une certaine machine. Si cela n'est pas fait, la machine s'arrêtera finalement - jusqu'à ce qu'elle soit lubrifiée et redémarrée.

Sans cette crainte, le processus de travail sur les programmes est beaucoup plus calme qu'avant. Les demandes d'extraction ne sont pas retardées jusqu'à la dernière. Le système CI / CD exécutera les tests et si les tests échouent, il arrêtera le processus de modification du code du projet. Dans le même temps, il sera très difficile de ne pas remarquer les messages d'erreur et les informations sur l'endroit exact où ils se sont produits.

C'est tout l'intérêt.

Chers lecteurs! Utilisez-vous TDD lorsque vous travaillez sur vos projets?

Source: https://habr.com/ru/post/fr450316/


All Articles