Skip to content

La philosophie d'ingénierie de Red Sift : tester vite, tester dur

L'équipe d'ingénierie de Red Sift partage notre approche des tests logiciels — ce que « vite » et « dur » signifient en pratique et pourquoi cela rend le produit plus fiable.

Red Sift
Published: April 30, 2019·Updated: March 18, 2024·14 min read

Une version de cet article de blog a été publiée à l'origine par Test MagazineTest Magazine en mars 2019, nous le partageons à nouveau pour ceux qui l'auraient manqué la première fois !

Developers like flame wars. Last Friday, when the afternoon was slowing in the office, an inadvertent GIF on Slack sparked some friendly debate about the right way to test. Because winning flame wars is important, here is my “well, actually”… 

À un moment donné dans les années 2000, alors que PHP n'était plus seulement considéré comme un langage de templating et que Ruby venait tout juste d'adopter Rails, la communauté des développeurs a décidé que les langages à typage dynamique étaient un excellent moyen de réduire la charge cognitive du programmeur, de rester résolument pragmatique et d'éviter la culture d'usine de Java.

Le mantra de Facebook « Move fast and break things » (« Avancer vite et casser des choses ») s'est depuis perpétué dans tout l'écosystème des startups, et est considéré comme la façon de construire une entreprise, des produits, des maisons, ou tout ce qui, en réalité, peut s'effondrer sur vous. Et si vous avez déjà travaillé sur du logiciel, vous savez que ce qui peut arriver, arrivera.

En chemin est arrivé Clean Code, et la montée du TDD a rassemblé une foule autour de l'écriture des tests en premier, de la croyance en la Couverture, et d'autres mantras qui sont amusants à réciter, mais nettement moins amusants à appliquer.

Beaucoup de livres de développement personnel comme Clean Code sont vaguement basés sur l'expérience personnelle, et introduisent certains modèles de programmation dans des domaines où ils n'avaient traditionnellement pas été utilisés : par exemple, simplifier l'orientation objet à la C++ avec des concepts de programmation fonctionnelle tels que des fonctions petites, simples et pures.

En réalité, il y a eu beaucoup de recherches sur la crise du logiciel et sur la façon de sortir du pétrin dans lequel nous sommes, et cela contredit souvent la sagesse populaire. Examinons différentes stratégies qui influencent la qualité du logiciel, et où elles font réellement une différence.

De bas en haut, j'examine généralement la vérification logicielle aux niveaux suivants : le système de types, les tests unitaires, les tests d'intégration, et la structure de gestion organisationnelle.

Attendez, quoi ? La structure de gestion organisationnelle ? Eh bien, peut-être pouvons-nous commencer par là.

Management

Le comportement organisationnel est une science sociale à part entière, et étudie l'art subtil des structures de gestion des personnes. Comme le logiciel est généralement créé par des humains, ils ont des besoins, des motivations internes et externes, et doivent parfois travailler ensemble pour livrer un livrable.

“quantifiable results from the study show that team and collaboration structure can be a better predictor of quality than tooling, testing strategies, or other code-based metrics.” 

Google a mené une étude sur ses équipes en tant qu'unités de travail pour identifier ce qui les rendait plus efficaces, mais la recherche de Microsoft s'est concentrée sur la façon dont la structure organisationnelle déterminait les taux d'échec des logiciels. Les deux sont des approches intéressantes à leur manière, et l'étude de Microsoft nous donne un aperçu intéressant du développement de Windows Vista. L'étude nous apprend que des équipes plus petites et plus focalisées produisent des logiciels plus fiables. Un taux de rotation élevé des ingénieurs abaisse la qualité du logiciel, tandis qu'une collaboration plus étroite entre les équipes travaillant sur le même projet entraîne des taux d'échec plus faibles.

Cela peut sembler des affirmations dignes du Capitaine Évidence (ou de Melvin Conway), cependant, les résultats quantifiables de l'étude montrent que la structure de l'équipe et de la collaboration peut être un meilleur indicateur de qualité que l'outillage, les stratégies de test, ou d'autres mesures basées sur le code.

La façon dont chaque équipe individuelle contrôle la qualité de sa production est l'étape suivante à partir d'ici. Les revues de code en particulier sont un excellent moyen de créer et de maintenir un ensemble commun de normes. Les revues de code écrites obligent les ingénieurs à communiquer clairement leurs préoccupations et cette communication technique accrue aidera tout le monde dans l'équipe à découvrir différents styles et perspectives, tout en aidant à niveler les compétences au sein de l'équipe. Peut-être évitez tout de même le style de Linus Torvalds.

Tests d'intégration

Les tests d'intégration, sans surprise, testent l'intégration de composants ou de modules dans un système. Vous pouvez également tester l'intégration de modules intégrés, et ce sont des tortues jusqu'en bas.

Il est souvent plus facile d'écrire du code correct en isolation, donc un grand nombre de bugs surviennent aux limites du système. Valider les entrées et formater les sorties, ne pas vérifier les niveaux de permission, ou une mauvaise implémentation des schémas d'interface. Ce problème est amplifié par la tendance actuelle des microservices, où les versions d'interface peuvent se désynchroniser entre les différents services au sein du système.

À ce niveau, il vaut mieux écrire des tests de bout en bout pour les fonctionnalités, et essayer de tirer parti du fait que nous avons tant d'autres couches de protection contre les échecs que quelque chose finira par déclencher ces fils. En fait, la couverture de code dans les tests d'intégration s'avère ne pas être un indicateur fiable des taux d'échec.

Si vous regardez cela autrement, la production n'est qu'un grand test d'intégration. Une astuce que j'aime utiliser est de créer une instance de production muette, qui reçoit une partie du trafic réel de production, mais ne générera jamais de réponses aux utilisateurs. Avec suffisamment d'investissement dans une couche d'orchestration sans état, nous pouvons même tester en mode muet des sous-arbres de services à des endroits stratégiques, puis les activer et abandonner l'ancien sous-arbre une fois la charge de travail terminée.

Couplé aux principes derrière la construction de systèmes hautement observables, ce type d'environnement de test élimine beaucoup d'anxiété quant à ce qui se passe lorsque nous déployons en production, car la mute-prod recevra précisément les mêmes données. Plus nous exposons de boutons et de sondes dans les systèmes en direct, meilleure est la visibilité que nous obtenons sur les rouages internes.

Tests unitaires

Donc, pour intégrer des modules, nous voulons une certaine confiance dans le fait que les modules eux-mêmes fonctionnent conformément aux spécifications. C'est là qu'entrent en jeu les tests unitaires.

Les tests unitaires sont généralement rapides, et testent plus ou moins de manière exhaustive des éléments isolés et faciles à comprendre. À quelle vitesse ? Le dépôt master de Ruby on Rails exécute environ 67 tests, et 176 assertions par seconde.

En règle générale, un test devrait couvrir un scénario pouvant arriver à un module. En comparaison avec les tests d'intégration, la même étude de Niedermayr, Juergens et Wagner montre que la couverture de code au niveau des tests unitaires influence bien les taux d'échec, si elle est bien réalisée.

Une étude de 94 réalisée par Hutchins et al. affirme que des niveaux de couverture supérieurs à 90 % montraient de meilleurs taux de détection des défauts que des ensembles de tests plus petits, et des améliorations « significatives » se sont produites lorsque la couverture est passée de 90 % à 100 %.

Le mouvement BDD a cette pratique amusante de développer des spécifications et de les transformer directement en tests unitaires. Les avantages de tests unitaires clairs et lisibles par l'humain aident à documenter le code et facilitent une partie du transfert de connaissances qui doit se produire lorsque des développeurs vont et viennent inévitablement, ou que les exigences des composants existants changent.

Les tests unitaires, dans mon esprit, incluent également les tests générés de style QuickCheck. L'idée de QuickCheck est qu'au lieu d'avoir du code impératif du genre « si ceci alors cela » à parcourir, le programmeur peut lister les hypothèses qui doivent être vraies pour la sortie de la fonction étant donné certaines entrées. QuickCheck génère ensuite des tests qui tentent de falsifier ces hypothèses en utilisant l'implémentation, et, s'il en trouve une, la réduit à une entrée minimale qui les prouve fausses.

Il est intéressant de noter que le nombre de scénarios qu'un test unitaire doit généralement couvrir est fortement influencé par le langage de programmation dans lequel il est écrit. Ce qui m'amène nécessairement à discuter de la sainte guerre de flammes sur le typage statique et dynamique.

Systèmes de types

Hindley-Milner

Les systèmes de types statiques de style Hindley-Milner tels que Haskell ou Rust obligent le programmeur à établir des contrats qui sont vérifiés avant que le programme puisse être exécuté.

Ce que le programmeur découvre alors, c'est qu'il programme soudainement 2 langages en parallèle : le système de types fournit une preuve pour le programme, tandis qu'il existe également un programme qui remplit les exigences.

Cela permet un style de raisonnement sur la justesse qui, couplé à un compilateur utile, permettra au programmeur de se concentrer sur des choses qui ne peuvent pas être prouvées par le système de types : la logique métier.

Bien sûr, ce n'est pas un gain net. Dans de nombreux cas, une solution utilisant un système de types dynamique est beaucoup plus simple et élégante, ou, dans d'autres cas, les contraintes du système de types rendent certaines implémentations pratiquement impossibles.

Dans d'autres cas, l'utilisation de Haskell a permis d'écrire un programme beaucoup plus petit, significativement plus rapidement par rapport aux alternatives, à tel point que la marine américaine a dû répéter le test par incrédulité.

L'élégance est dans l'œil de celui qui regarde, et une abstraction magnifiquement typée qui se réduit à une simple machine à états lors de la compilation peut être tout aussi attrayante qu'une macro LISP rapide et sale. Parfois, toute cette complexité est difficile à justifier uniquement pour plaire au compilateur. Cela vient de l'expérience, du goût, et de l'application d'un jugement raisonné dans la bonne situation. Les gens considèrent souvent la programmation comme un artisanat. Oui, nous savons faire le calcul, mais avec cette astuce astucieuse, nous arrivons à 90 % du résultat avec 10 % de l'effort, et cela peut être suffisant. Et cela va exploser dans ce cas limite que je pensais ne jamais pouvoir réellement se produire. Mais je m'égare.

Faible mais statique

Un peu plus accessibles, mais fournissant une vérification moins stricte, se trouvent les langages de la famille OOP style C++/Java/C#, ainsi que des langages comme C et Go.

Les systèmes de types ici permettent différents types de flexibilité, et des échappatoires souhaitables vers le monde dynamique.

Un système de types plus faible, mais toujours statique, offre moins de garanties sur la justesse des programmes, quelque chose que nous devons compenser par les tests, et/ou des normes de codage. Le Jet Propulsion Lab de la NASA, un fabricant de masse de rovers martiens, maintient un ensemble de directives de programmation sécurisée pour C. Leurs directives semblent efficaces. Opportunity a dépassé ses 90 jours d'activité initialement prévus de 14 ans grâce à une maintenance minutieuse. Curiosity parcourt toujours la surface de Mars, et est patché régulièrement.

Dynamique

En parlant du JPL, le folklore d'Internet préserve l'histoire de l'utilisation de LISP au laboratoire de la NASA, un langage de programmation fonctionnel et dynamique des années 60, qui est toujours considéré comme l'une des inventions, voire des découvertes, les plus influentes de l'informatique. Le dialecte LISP le plus couramment utilisé aujourd'hui est Clojure, qui gagne en popularité dans les cercles de la science des données.

Les langages dynamiques offrent une liberté ultime, et peu de sécurité. Le plus souvent, la seule façon de déterminer si un morceau de code est raisonnable est de l'exécuter, ce qui signifie que notre stratégie de test doit être plus rigoureuse, et, en effet, approfondie, car il n'y a pas de couche suivante sur laquelle se rabattre.

L'usage le plus répandu d'un système de types dynamique aujourd'hui vient probablement de JavaScript. Il est intéressant de noter que, alors que les entreprises s'efforcent de réduire la barrière d'entrée pour les contributeurs tout en développant le développement et en maintenant la qualité, Google, Microsoft et Facebook ont tous proposé leurs propres solutions pour introduire une forme de vérification de type statique dans le langage.

Même si Dart de Google n'a pas connu une adoption significative, TypeScript de Microsoft l'a fait, et son utilisation est portée par des projets vastes et populaires tels que VSCode. Les deux approches introduisent un langage avec vérification de type statique qui se compile en JavaScript, ce qui facilite également son introduction progressive dans des projets existants.

En revanche, Flow de Facebook est un analyseur statique purement construit sur JavaScript, qui introduit son propre type d'annotations de types. L'idée est que si des annotations de type sont présentes à des endroits stratégiques, le vérificateur de types devrait être en mesure de déterminer s'il y a des erreurs de type dans une partie du programme en traçant le flux de données.

Les programmeurs enthousiastes vous diront que les deux approches du typage statique en JavaScript sont défectueuses à leur manière, et ils auraient raison. En fin de compte, beaucoup d'arguments se résument à des idées et des goûts subjectifs sur l'architecture logicielle. Il semble cependant difficile de nier qu'une certaine forme de vérification de type statique offre plusieurs avantages pour la mise à l'échelle et la maintenance des projets logiciels.

Les petites choses qui nous échappent

La liste des choses que nous pouvons faire pour garantir la justesse des logiciels est loin d'être terminée. L'état de l'art continue de progresser, et de nouvelles approches gagnent rapidement en popularité, notamment au sein de la communauté de la sécurité.

Dans les modules absolument critiques, comme tout ce qui concerne la cryptographie ou la sécurité, la vérification formelle peut accroître la confiance dans certaines parties du système, mais elle est difficile à mettre à l'échelle.

Un sentiment familier se retrouve derrière les principes de LangSec. Dans de nombreux cas, la puissance et l'expressivité de nos langages permettent à des bugs involontaires de s'infiltrer. LangSec dit : rendez tous les états invalides irreprésentables par le langage lui-même. Faites en sorte que le langage limite ce que le programmeur peut faire, afin qu'il puisse éviter ce qu'il ne devrait pas faire. C'est également la motivation derrière des normes de codage telles que celles du JPL, qui permettent de raisonner plus facilement sur l'état et le flux de données tout au long du code du programme.

Lorsque nous sommes raisonnablement sûrs que ce dont nous avons besoin est suffisamment bon, nous pouvons commencer à le fuzzer. Le fuzzing, c'est génial. Il s'agit d'injecter des états inattendus dans un système, et d'attendre qu'ils provoquent des échecs. Cette idée simple aide à découvrir des failles de sécurité dans des logiciels populaires ou peut aider à concevoir du chaos dans le cloud.

Comme toujours, produire un système stable et sécurisé nécessite une ingénierie rigoureuse, tant en logiciel qu'en architecture. Nous devons comprendre les éléments qui composent l'ensemble, analyser puis vérifier leurs interactions en interne, et avec l'environnement. Malgré nos meilleurs efforts, des bugs s'infiltreront toujours, et tout ce que nous pouvons faire, c'est essayer de garantir que ceux qui subsistent ne soient pas catastrophiques.

Cependant, une fois le logiciel mis en production, la vérification ne s'arrête pas. Concevoir pour l'observabilité en exposant des boutons, du traçage, des alertes, et en collectant un ensemble de métriques opérationnelles nous aide tous à raisonner sur l'état du système pendant son fonctionnement, ce qui est le test ultime de tout cela.

Le développement logiciel est un processus, et il est pratiquement impossible d'atteindre la perfection. Tant que l'équipe a un plan pour s'en approcher, et que tout le monde est engagé, nous pouvons considérer que c'est suffisamment bon, puis sortir du bureau pour profiter du soleil.

Si vous êtes intéressé à débattre davantage des bonnes et des mauvaises pratiques en matière de tests logiciels, envoyez-moi un message – @rhapsodhy ou par email – j'aimerais beaucoup connaître votre avis !

Ici chez Red Sift, lorsque nous ne débattons pas de la bonne façon de tester, nous permettons aux organisations axées sur la sécurité de communiquer avec succès et d'assurer la confiance de leurs employés, fournisseurs et clients. Consultez notre page d'accueil pour en savoir plus sur ce que nous faisons.

Red Sift find out moreRed Sift find out more
Red Sift
Red Sift