Rejoindre toute nouvelle entreprise – avec une culture et des pratiques de programmation établies – peut être une expérience intimidante. Lorsque j’ai rejoint l’équipe Ansible, j’ai décidé de rédiger les pratiques et les principes d’ingénierie logicielle que j’ai appris au fil des ans et auxquels je m’efforce de travailler. Il s’agit d’une liste non définitive et non exhaustive de principes qui doivent être appliqués avec sagesse et flexibilité.
Ma passion est pour les tests, car je pense que de bonnes pratiques de test peuvent à la fois garantir un standard de qualité minimum (malheureusement absent de nombreux logiciels), et guider et façonner le développement lui-même. Beaucoup de ces principes se rapportent aux pratiques et idéaux de test. Certains de ces principes sont spécifiques à Python, mais la plupart ne le sont pas. (Pour les développeurs Python, PEP 8 devrait être votre premier arrêt pour le style de programmation et les directives.)
En général, nous, les programmeurs, sommes un lot d’opinions, et les opinions fortes sont souvent le signe d’une grande passion. Dans cet esprit, n’hésitez pas à ne pas être d’accord avec ces points, et nous pouvons en discuter et en débattre dans les commentaires.
Bonnes pratiques de développement et de test
1. YAGNI : » You Aint Gonna Need It « . N’écrivez pas de code dont vous pensez avoir besoin à l’avenir, mais dont vous n’avez pas encore besoin. C’est coder pour des cas d’utilisation futurs imaginaires, et inévitablement, le code deviendra du code mort ou devra être réécrit parce que le cas d’utilisation futur s’avère toujours fonctionner légèrement différemment de la façon dont vous l’avez imaginé.
Si vous mettez du code pour un cas d’utilisation futur, je le remettrai en question lors d’une revue de code. (Vous pouvez, et devez, concevoir des API, par exemple, pour permettre des cas d’utilisation futurs, mais c’est une autre question.)
C’est la même chose pour le code commenté ; si un bloc de code commenté va dans une version, il ne devrait pas exister. Si c’est du code qui peut être restauré, faites un ticket et référencez le hash du commit pour la suppression du code. YAGNI est un élément central de la programmation agile. La meilleure référence à ce sujet est Extreme Programming Explained, de Kent Beck.
2. Les tests n’ont pas besoin d’être testés. L’infrastructure, les frameworks et les bibliothèques pour les tests ont besoin de tests. Ne testez pas le navigateur ou les bibliothèques externes, sauf si vous en avez vraiment besoin. Testez le code que vous écrivez, pas le code des autres.
3. La troisième fois que vous écrivez le même morceau de code est le bon moment pour l’extraire dans une aide à usage général (et écrire des tests pour celle-ci). Les fonctions d’aide à l’intérieur d’un test n’ont pas besoin d’être testées ; lorsque vous les cassez et les réutilisez, elles ont besoin de tests. À la troisième fois que vous avez écrit un code similaire, vous avez tendance à avoir une idée claire de la forme du problème à usage général que vous résolvez.
4. Quand il s’agit de la conception de l’API (facing externe et API objet) : Les choses simples doivent être simples ; les choses complexes doivent être possibles. Concevez d’abord pour le cas simple, avec de préférence zéro configuration ou paramétrage, si c’est possible. Ajoutez des options ou des méthodes API supplémentaires pour les cas d’utilisation plus complexes et plus flexibles (selon les besoins).
5. Échec rapide. Vérifiez l’entrée et échouez sur une entrée insensée ou un état invalide dès que possible, de préférence avec une exception ou une réponse d’erreur qui rendra le problème exact clair pour votre appelant. Permettez des cas d’utilisation « innovants » de votre code cependant (c’est-à-dire, ne faites pas de vérification de type pour la validation d’entrée à moins que vous n’en ayez vraiment besoin).
6. Les tests unitaires testent l’unité de comportement, pas l’unité d’implémentation. Changer l’implémentation, sans changer le comportement ou avoir à changer l’un de vos tests est l’objectif, bien que ce ne soit pas toujours possible. Donc, dans la mesure du possible, traitez vos objets de test comme des boîtes noires, en testant à travers l’API publique sans appeler les méthodes privées ou bricoler l’état.
Pour certains scénarios complexes – comme tester le comportement sur un état complexe spécifique pour trouver un bug obscur – cela peut ne pas être possible. Écrire les tests en premier aide vraiment à cela, car cela vous oblige à penser au comportement de votre code et à la façon dont vous allez le tester avant de l’écrire. Le fait de tester en premier encourage les unités de code plus petites et plus modulaires, ce qui signifie généralement un meilleur code. Une bonne référence pour démarrer avec l’approche « test first » est Test Driven Development by Example, de Kent Beck.
7. Pour les tests unitaires (y compris les tests d’infrastructure de test), tous les chemins de code doivent être testés. Une couverture de 100 % est un bon point de départ. Vous ne pouvez pas couvrir toutes les permutations/combinaisons d’état possibles (explosion combinatoire), donc cela nécessite une réflexion. Ce n’est que s’il y a une très bonne raison que les chemins de code ne doivent pas être testés. Le manque de temps n’est pas une bonne raison et finit par coûter plus de temps. Parmi les bonnes raisons possibles, on peut citer : l’impossibilité de tester le code (d’une manière significative), l’impossibilité de l’atteindre en pratique ou le fait qu’il soit couvert par un autre test. Le code sans tests est un handicap. Mesurer la couverture et rejeter les RP qui réduisent le pourcentage de couverture est un moyen de s’assurer que vous progressez progressivement dans la bonne direction.
8. Le code est l’ennemi : il peut mal tourner, et il a besoin de maintenance. Écrivez moins de code. Supprimez du code. N’écrivez pas de code dont vous n’avez pas besoin.
9. Inévitablement, les commentaires de code deviennent des mensonges avec le temps. En pratique, peu de gens mettent à jour les commentaires lorsque les choses changent. Efforcez-vous de rendre votre code lisible et auto-documenté grâce à de bonnes pratiques de nommage et à un style de programmation connu.
Le code qui ne peut pas être rendu évident – contourner un bogue obscur ou une condition improbable, ou une optimisation nécessaire – doit être commenté. Commentez l’intention du code, et pourquoi il fait quelque chose plutôt que ce qu’il fait. (Ce point particulier sur les commentaires comme mensonges est controversé, d’ailleurs. Je pense toujours qu’il est correct, et Kernighan et Pike, auteurs de The Practice of Programming, sont d’accord avec moi.)
10. Écrivez de manière défensive. Pensez toujours à ce qui peut mal tourner, à ce qui se passera en cas d’entrée invalide, et à ce qui pourrait échouer, ce qui vous aidera à attraper de nombreux bugs avant qu’ils ne se produisent.
11. La logique est facile à tester en unité si elle est sans état et sans effet secondaire. Répartissez la logique dans des fonctions distinctes, plutôt que de mélanger la logique dans du code stateful et rempli d’effets secondaires. La séparation du code avec état et du code avec effets secondaires en fonctions plus petites les rend plus faciles à simuler et à tester sans effets secondaires. (Moins de frais généraux pour les tests signifie des tests plus rapides.) Les effets secondaires nécessitent des tests, mais les tester une fois et les mocker partout ailleurs est généralement un bon modèle.
12. Les globaux sont mauvais. Les fonctions sont meilleures que les types. Les objets sont probablement meilleurs que les structures de données complexes.
13. Utiliser les types intégrés à Python – et leurs méthodes – sera plus rapide que d’écrire vos propres types (sauf si vous écrivez en C). Si les performances sont une considération, essayez de trouver comment utiliser les types intégrés standard plutôt que des objets personnalisés.
14. L’injection de dépendances est un patron de codage utile pour être clair sur ce que sont vos dépendances et d’où elles viennent. (Faites en sorte que les objets, les méthodes et ainsi de suite reçoivent leurs dépendances en tant que paramètres plutôt que d’instancier eux-mêmes de nouveaux objets). Cela rend les signatures d’API plus complexes, c’est donc un compromis. Si vous vous retrouvez avec une méthode qui nécessite 10 paramètres pour toutes ses dépendances, c’est un bon signe que votre code en fait trop, de toute façon. L’article définitif sur l’injection de dépendances est » Inversion of Control Containers and the Dependency Injection Pattern « , par Martin Fowler.
15. Plus vous devez mocker pour tester votre code, plus votre code est mauvais. Plus vous devez instancier et mettre en place du code pour pouvoir tester un élément de comportement spécifique, plus votre code est mauvais. L’objectif est de petites unités testables, ainsi que des tests d’intégration et fonctionnels de plus haut niveau pour tester que les unités coopèrent correctement.
16. Les API tournées vers l’extérieur sont celles où la » conception en amont » – et la prise en compte des cas d’utilisation futurs – compte vraiment. Changer d’API est une douleur pour nous et pour nos utilisateurs, et créer une incompatibilité rétroactive est horrible (bien que parfois impossible à éviter). Concevez soigneusement les API tournées vers l’extérieur, tout en respectant le principe » les choses simples doivent être simples « .
17. Si une fonction ou une méthode dépasse 30 lignes de code, envisagez de la décomposer. Une bonne taille maximale de module est d’environ 500 lignes. Les fichiers de test ont tendance à être plus longs que cela.
18. Ne faites pas de travail dans les constructeurs d’objets, qui sont difficiles à tester et surprenants. Ne mettez pas de code dans __init__.py (sauf les importations pour l’espacement des noms). __init__.py n’est pas l’endroit où les programmeurs s’attendent généralement à trouver du code, c’est donc » surprenant « .
19. DRY (Don’t Repeat Yourself) importe beaucoup moins dans les tests que dans le code de production. La lisibilité d’un fichier de test individuel est plus importante que la maintenabilité (casser des morceaux réutilisables). Cela s’explique par le fait que les tests sont exécutés et lus individuellement plutôt que de faire partie d’un système plus vaste. Évidemment, une répétition excessive signifie que des composants réutilisables peuvent être créés pour la commodité, mais c’est beaucoup moins une préoccupation que pour la production.
20. Refactoriser chaque fois que vous en voyez le besoin et que vous en avez la possibilité. La programmation est une question d’abstractions, et plus vos abstractions correspondent au domaine du problème, plus votre code est facile à comprendre et à maintenir. Au fur et à mesure que les systèmes se développent organiquement, ils doivent changer de structure pour s’adapter à l’expansion de leurs cas d’utilisation. Les systèmes dépassent leurs abstractions et leur structure, et ne pas les changer devient une dette technique qui est plus pénible (et plus lente et plus boguée) à gérer. Incluez le coût de l’élimination de la dette technique (remaniement) dans les estimations du travail sur les fonctionnalités. Plus vous laissez la dette en place, plus les intérêts s’accumulent. Un excellent livre sur le refactoring et les tests est Working Effectively with Legacy Code, de Michael Feathers.
21. Rendez le code correct d’abord et rapide ensuite. Lorsque vous travaillez sur des problèmes de performance, établissez toujours un profil avant de faire des corrections. Habituellement, le goulot d’étranglement n’est pas tout à fait là où vous pensiez qu’il était. Écrire du code obscur parce qu’il est plus rapide ne vaut la peine que si vous avez profilé et prouvé que cela en vaut vraiment la peine. Écrire un test qui exerce le code que vous profilez avec un timing autour de lui permet de savoir plus facilement quand vous avez fini, et peut être laissé dans la suite de tests pour prévenir les régressions de performance. (Avec la remarque habituelle que l’ajout de code de chronométrage change toujours les caractéristiques de performance du code, ce qui fait du travail de performance l’une des tâches les plus frustrantes.)
22. Des tests unitaires plus petits et plus étroitement cadrés donnent des informations plus précieuses lorsqu’ils échouent – ils vous disent spécifiquement ce qui ne va pas. Un test qui se dresse sur la moitié du système pour tester le comportement prend plus d’investigation pour déterminer ce qui ne va pas. En général, un test qui prend plus de 0,1 seconde pour s’exécuter n’est pas un test unitaire. Il n’y a pas de test unitaire lent. Avec des tests unitaires de portée étroite pour tester le comportement, vos tests agissent comme une spécification de facto pour votre code. Idéalement, si quelqu’un veut comprendre votre code, il devrait pouvoir se tourner vers la suite de tests comme « documentation » du comportement. Une excellente présentation sur les pratiques de test unitaire est Fast Test, Slow Test, par Gary Bernhardt :
23. » Not Invented Here » n’est pas aussi mauvais que les gens le disent. Si nous écrivons le code, alors nous savons ce qu’il fait, nous savons comment le maintenir, et nous sommes libres de l’étendre et de le modifier comme bon nous semble. Ceci est conforme au principe YAGNI : nous disposons d’un code spécifique pour les cas d’utilisation dont nous avons besoin plutôt que d’un code à usage général qui présente une complexité pour des choses dont nous n’avons pas besoin. D’un autre côté, le code est l’ennemi, et posséder plus de code que nécessaire est mauvais. Considérez le compromis lorsque vous introduisez une nouvelle dépendance.
24. La propriété partagée du code est l’objectif ; la connaissance en silo est mauvaise. Au minimum, cela signifie discuter ou documenter les décisions de conception et les décisions de mise en œuvre importantes. La revue de code est le pire moment pour commencer à discuter des décisions de conception, car l’inertie pour apporter des changements radicaux après l’écriture du code est difficile à surmonter. (Bien sûr, il est toujours préférable de signaler et de modifier les erreurs de conception au moment de la révision que de ne jamais le faire.)
25. Les générateurs rock ! Ils sont généralement plus courts et plus faciles à comprendre que les objets à état pour l’itération ou l’exécution répétée. Une bonne introduction aux générateurs est « Generator Tricks for Systems Programmers », par David Beazley.
26. Soyons des ingénieurs ! Pensons à concevoir et à construire des systèmes robustes et bien implémentés, plutôt que de faire pousser des monstres organiques. La programmation est cependant un exercice d’équilibre. Nous ne sommes pas toujours en train de construire une fusée. Une ingénierie excessive (architecture en oignon) est aussi pénible à gérer qu’un code mal conçu. La plupart des ouvrages de Robert Martin valent la peine d’être lus, et Clean Architecture : A Craftsman’s Guide to Software Structure and Design est une bonne ressource sur ce sujet. Design Patterns est un livre de programmation classique que tout ingénieur devrait lire.
27. Les tests qui échouent par intermittence érodent la valeur de votre suite de tests, au point que finalement tout le monde ignore les résultats des tests parce qu’il y a toujours quelque chose qui échoue. Corriger ou supprimer les tests qui échouent par intermittence est douloureux, mais vaut la peine d’être fait.
28. En général, en particulier dans les tests, attendez un changement spécifique plutôt que de dormir pendant une durée arbitraire. Les sleeps voodoo sont difficiles à comprendre et ralentissent votre suite de tests.
29. Voyez toujours votre test échouer au moins une fois. Mettez un bug délibéré et assurez-vous qu’il échoue, ou exécutez le test avant que le comportement testé soit complet. Sinon, vous ne savez pas si vous testez vraiment quelque chose. Écrire accidentellement des tests qui ne testent en fait rien ou qui ne peuvent jamais échouer est facile.
30. Et enfin, un point pour la direction : Le feature grind constant est une façon terrible de développer des logiciels. Ne pas laisser les développeurs être fiers de leur travail garantit que vous ne tirerez pas le meilleur d’eux. Ne pas s’occuper de la dette technique ralentit le développement et aboutit à un produit plus mauvais et plus bogué.
La dette technique est un élément essentiel du développement.