Améliorer la qualité avec les tests et la review

L’importance des tests et de la revue de code dans le cadre du développement logiciel est parfois négligée ou passée au second plan. Cet article a pour but de montrer que les tests logiciels constituent une étape cruciale qu’il faut considérer avec beaucoup de rigueur.

Nous allons ainsi revenir sur quelques façons de procéder pour tester et valider les nouvelles fonctionnalités développées, donner des exemples de bonnes pratiques (et de bugs !), et essayer d’insister sur les points de détails auxquels on ne pense pas assez souvent pendant les tests et la review. Le but étant d’être instructif, original, un peu amusant aussi 😛, et bien sûr d’aider à avoir des tests plus complets, pour faire les choses bien !

Tester : tout un art

Je pense que c’est bien de le rappeler : les tests font partie intégrante du développement. (Voilà, c’est bon, il fallait le dire au moins une fois !).

Différents types de tests

Il existe bien sûr différents types de tests, et c’est intéressant d’en avoir à plusieurs niveaux, car tous seront complémentaires. Le but n’est pas d’en faire une liste exhaustive, mais voyons-en quelques-uns.

Au plus proche des algorithmes, on aura les tests unitaires, qui ont pour but de tester une fonction, un bout d’algorithme bien précis. Ils sont importants pour valider un développement et pour s’assurer qu’on n’a pas introduit de régression dans une fonctionnalité existante. Plus haut, on aura les cas d’utilisation (use cases), qui reproduisent des cheminements courants des utilisateurs, permettant de tester le logiciel dans une utilisation plus complète. Ils peuvent être utilisés pour faire des tests globaux (intégration, end-to-end…), où l’on ne teste plus une petite partie spécifique mais plusieurs parties ensemble ou même l’ensemble de l’application : quand les tests unitaires spécifiques permettent de valider des morceaux de code, les tests d’intégration vont déterminer si l’application en elle-même fonctionne et est utilisable.

On peut également penser aux tests du client, le cas échéant, en le mettant dans la boucle et en l’invitant à tester chaque fois que c’est nécessaire, pour vérifier si ce qui a été développé correspond bien aux attentes, pour voir si rien n’a été oublié dans les spécifications. Cela se fait préférablement pendant le stade du développement (toujours dans l’optique de régler les problèmes le plus tôt possible) ; une phrase connue dans le monde du développement, c’est que « plus un bug est découvert tard, plus il coûte cher à corriger », et, spoiler, c’est vrai 😛.

Enfin, on peut permettre aux utilisateurs de reporter des erreurs et avoir un système de tracking d’erreurs (de type Sentry), lorsque cela est possible, par exemple dans le cas d’une application Web. Ce sont des moyens efficaces d’identifier des erreurs qui seraient passées entre les mailles des filets.

Quand les tests d’intégration passent sans erreur

De plus, la mise en place de tests automatiques est d’une grande utilité et fait gagner un temps précieux : on a besoin de moins de tests manuels (refaire à chaque fois les mêmes tests à la main, c’est hyper répétitif et sur un gros projet ça devient vite l’enfer), et à chaque évolution on pourra vérifier s’il n’y a pas de régression. Cela permet également de reporter des erreurs qui n’auraient pas nécessairement été identifiées lors d’un test non automatisé. Nous parlerons plus spécifiquement des tests automatiques pour les logiciels 3d dans un futur article (ce sera hyper intéressant, vous verrez).

Pousser un peu les tests

À chaque ajout de fonctionnalité ou évolution de fonctionnalités existantes, il vaut mieux tester aussi d’autres choses différentes, tout ce qui s’y rapporte de près ou de loin, ou même qui n’ont rien à voir ; en faisant à nouveau tout un cheminement complet, avec différents jeux de tests et scénarios. Être un peu paranoïaque, en somme (enfin, rigoureux plutôt, mais parfois ça passe pour de la paranoïa…).

Ouais, mais bon, j’ai ajouté que quelques lignes de code dans un seul fichier, y’a pas de raison que…

Une modification, même infime, à un endroit précis pour faire évoluer une fonctionnalité peut en impacter une autre qui utilisait les mêmes éléments ou qui utilisait la fonctionnalité en question. Une bonne pratique à avoir consiste à toujours tester même si peu de modifications entrent en jeu et même si tout semble bon de prime abord. De plus, on peut analyser quels impacts pourraient avoir les modifications, pour mieux cibler ce qui est susceptible d’avoir été involontairement modifié. Et on évite aussi des commentaires du genre :

— Ah ça a marché !
— Comment tu sais ça ?
— Ben ça a cassé des trucs…

Tester à l’extrême

Pour continuer à insister sur les tests, on peut aussi parler des cas particuliers, incohérents, impossibles, invalides, extrêmes… C’est (pas si étonnamment que ça) souvent un excellent moyen de trouver des bugs (et ce, rapidement). Bref, des cas bien foireux, en se mettant dans la peau d’un utilisateur soit vraiment pas doué, soit qui n’a pas toute sa tête…

 

Un cas incohérent ou invalide très basique consiste à entrer une valeur textuelle dans un champ numérique, ou entrer des valeurs numériques négatives/nulles/très élevées pour voir comment le logiciel réagit. Plus généralement, il s’agit de tester des utilisations illogiques. Dans le logiciel Web de création de plans de maisons, Kazaplan, que nous développons ici, on pourrait par exemple essayer d’ajouter une porte alors qu’on n’a pas de mur… mais cela aura pour seul effet d’afficher un message à l’utilisateur 😉.

On aide les architectes en herbe, en fait…

Selon la taille de l’application, les différentes actions possibles peuvent s’avérer nombreuses, ainsi l’utilisateur dispose d’une certaine liberté de mouvement. Pour un cas extrême, prenons l’exemple d’un bug qui n’est visible qu’en faisant un drag tout en utilisant un raccourci clavier et en passant le zoom à 65,421%… ou encore faire un faceroll sur le clavier, en utilisant tous les boutons de la souris, et tous les raccourcis disponibles, tout en invoquant du vaudou… ou plus généralement (et plus scientifiquement ?), un ensemble d’actions qui individuellement peuvent être valides mais qui simultanément provoquent un comportement inattendu.

Non, sérieusement, après rétro-ingénierie de certains bugs, on se demande…

Même si le cheminement pour y arriver peut sembler particulier, étrange, improbable, voire même complètement abusé, il y a toujours une raison logique derrière les anomalies détectées. Il est important de garder à l’esprit, quand on développe des logiciels qui seront utilisés par la suite par un grand nombre d’utilisateurs, que le genre de manipulations ou d’ensemble d’actions qui paraissent improbables aux développeur(euse)s peuvent devenir statistiquement réalistes lorsque le nombre d’utilisateurs augmente. Donc si vous trouvez un bug, il est fort probable que quelqu’un tombe dessus aussi (par hasard, par fausse manipulation ; un utilisateur ne sait pas forcément comment utiliser le logiciel et ses fonctionnalités, donc il peut ne pas faire les actions comme il faudrait, comme attendu, ou d’une façon qui nous paraît peu logique). Cela peut paraître extrême, mais les cas d’erreurs trouvés peuvent souvent se réduire à un cas plus simple, qu’on n’aurait pas nécessairement trouvé en testant « normalement »… Pour reproduire, simplifier, et arriver ainsi à un cas minimal, c’est alors un peu du reverse-testing (faites-moi penser à faire breveter ce terme). On couvre également beaucoup de cas à la fois avec ce genre de tests.

Alors, quand on dit « Nan mais ça, c’est impossible que quelqu’un tombe dessus… » :

Toute cette attention portée aux détails diminue encore le nombre d’erreurs.

Les détails font la perfection, et la perfection n’est pas un détail
— L. de Vinci

Exemples de bugs, systèmes critiques et répercussions

Si vous n’êtes pas encore convaincu par l’extreme-testing (ça aussi faut que je le fasse breveter), voici des exemples de bugs intéressants, que vous connaissez peut-être, et qui illustrent l’importance de vérifier les choses jusqu’au bout (et accessoirement, qui illustrent des trucs dont je viens de parler) :

  • Le Mew glitch (bug permettant de capturer Mew dans les premières versions de Pokémon, détaillé ici, par exemple), qui en apparence peut sembler très bizarre (voire complètement WTF, disons-le clairement), mais qui en fait s'explique de manière tout à fait logique (voici un exemple d’explication parmi d’autres). La marche à suivre contient des actions valides individuellement, mais utilisées dans un contexte où on n’aurait pas dû pouvoir les faire (par exemple, appuyer sur start au moment où on aurait dû avoir une rencontre avec un dresseur, ce qui l’initialise sans la provoquer), ce qui va donner en mémoire des valeurs erronées, provoquant un cas invalide et inattendu

  • Bug sur Godot Engine (où des éléments d'UI pouvaient être affichés comme étant pressed/released alors qu’ils étaient dans l’état inverse en utilisant plusieurs boutons de souris, par misclick notamment) : exemple qu’une fausse manipulation est vite arrivée, et peut donner lieu à des états non voulus, incohérents

  • Bugs liés à la date du 29 février (comme celui de la Playstation 3 en 2010 qui cherchait un 29 février inexistant), exemples parmi d’autres des problèmes posés par les cas particuliers

  • Bug du « Nuclear Gandhi » (oui, oui ! 😅): dans le premier jeu Civilization, où le personnage pouvait devenir agressif et guerrier, déterminé à rayer votre civilisation de la carte. Cela se produisait lorsque la valeur de son agressivité (un entier non signé sur 8 bits, initialement à 1 pour Gandhi) diminuait de 2 points (au passage à une forme de gouvernement moins « belliqueuse »), provoquant un overflow passant la valeur à 255, donnant ainsi au Gandhi du jeu la valeur maximale d’agressivité… bref, toujours faire attention aux bugs « classiques » du développement

  • « Astuce » dans Uncharted 2 lors de l’affrontement final : et si au lieu de fuir et d’esquiver continuellement, on s’accrochait dans un coin durant toute la partie ? Cela revient à faire les choses pas comme on est censé les faire, ou plutôt pas comme on attend qu’un utilisateur les fasse, une sorte de comportement illogique (et pour le coup vraiment moins sympa à jouer…)

  • On peut aussi citer la faille dans la structure de l’étoile de la mort. S’ils avaient review correctement et pensé à tout… *hum, hum*

Jusqu’à présent, nous avons cité des bugs qui n’ont pas eu de grandes conséquences. Mais les erreurs peuvent également provoquer des accidents, dans le cas de systèmes critiques. Parmi les bugs les plus (tristement) célèbres, on peut citer celui de Mars Climate Orbiter en 1999, un crash dû au logiciel de navigation qui n’utilisait pas les unités du système international contrairement à d’autres logiciels utilisés par la NASA. Une erreur un peu… évitable, surtout lorsque les coûts d’une telle entreprise s’élèvent à plusieurs dizaines de millions de dollars…

Il est capital de bien se comprendre et d’avoir les mêmes références pour la gestion des unités, de la date, de l’heure, ….

Dans le même registre, le « vol » 501 d’Ariane 5 en 1996 a vu la fusée exploser après quelques secondes seulement, à cause de l’overflow d’une variable (oui, encore), et a coûté environ 500 millions de dollars. Ou encore, le bug du missile Patriot durant la guerre du Golfe, où un missile défensif n’a pas pu intercepter un missile balistique tactique, provoquant la mort de 28 personnes et de nombreux blessés. Le code du logiciel embarqué prenait le temps en dixièmes de secondes mesuré par l’horloge interne du système, et le multipliait par 1/10 pour obtenir un temps en secondes. Or 0.1 n’est pas une valeur qui peut s’exprimer de manière exacte en binaire : le nombre 24 bits en mémoire était de 0.09999990463256835938 (0.00011001100110011001100 en binaire) ce qui correspond à une différence d’environ 9.537e-8. Cela peut paraître négligeable, mais au bout de 100 heures (360 000 secondes), on arrive à une différence de 0.34s. Problème : le missile balistique pouvant atteindre une vitesse de 1,7km/s, il peut parcourir plus de 500m en 0.34 seconde. La différence en apparence insignifiante en a donc provoqué une trop grande.

En résumé, on peut avoir des situations d’utilisation à grande échelle (par exemple avec plusieurs centaines de milliers de personnes qui doivent avoir accès à des fonctionnalités), avec beaucoup d’argent et/ou de répercussions en jeu, parfois liées à la sécurité physique. Il est donc impératif de bien tester les systèmes complexes, même si les tests peuvent devenir difficiles et longs, et pour des situations critiques, les tests sont d’autant plus importants.

Les erreurs peuvent donc être coûteuses en temps (pour régler les problèmes après coup, pour essayer de reproduire les erreurs qui sont signalées…). En testant mieux, on prend plus de temps, mais on en gagne sur le long terme : on s’assure d’être juste, on évite une remontée d’erreur et le temps d’un nouveau processus de correction/validation, ainsi qu’une éventuelle gêne occasionnée. On gagne aussi en relations humaines (en évitant un logiciel livré avec beaucoup de bugs et en soignant la réputation de fiabilité).

Et d’une manière générale, c’est bien de garder en tête que derrière, il y a des gens. Que ce soit pour un téléphone, une voiture, un jeu vidéo, ou n’importe quel type de logiciel, il peut être amené à être utilisé par un grand nombre de personnes, dans différentes situations, et il faut avoir l’envie du travail bien fait (#Responsabilité). Cela s’applique à tous les domaines, scientifiques ou non, « tester » pour trouver les failles dans un logiciel, un vélo, un journal avant parution… Vraiment tous les domaines.

 

Si vous voyez ce que je veux dire…

Tests et retours : comment faire une bonne revue de code ?

Tout d’abord, c’est pratique d’avoir un processus de revue de code (code review) bien défini. La revue de code désigne l’ensemble des vérifications effectuées pour valider un développement. On prendra ici appui sur les merge requests de GitLab ou les pull requests de GitHub, où on peut :

  • Tester chaque nouveau développement de manière indépendante

  • Voir les modifications de code et faire des retours à l’auteur(e) de la merge request, pour des questions, propositions ou corrections à effectuer

  • Donner une validation

Il est préférable de ne pas faire d’exceptions, même (surtout ?) pour un hotfix d’une ligne en apparence simple… Aller vite sans trop regarder est un des meilleurs moyens d’introduire des bugs. C’est aussi important d’avoir plusieurs testeurs : à force de travailler sur notre propre développement, on peut apporter involontairement une sorte de biais, en testant soi-même les mêmes choses et de la même façon. La review permet d'apporter un regard nouveau, d'autres façons de tester, des tests auxquels on n'aurait pas pensé. C’est pourquoi on considère généralement que plusieurs validations (au moins deux) par des personnes n’ayant pas participé au développement sont nécessaires. (Et si on travaille seul sur un projet, il faut quand même avoir un processus de test/validation bien carré, mais on ne s’attardera pas là-dessus ici).

Au cours d’une revue de code, il faut penser à plein de choses ! Ça tombe bien, la suite aborde différents aspects de ce qu’on doit vérifier lors de la review, et la façon de faire des retours (vraiment bien fait, cet article !).

Tests logiciels

Avant tout, il faut s’assurer de savoir ce qu’on teste, de bien avoir compris le contexte et les attentes, et de savoir comment tester le tout. On peut se retrouver à tester quelque chose d’assez inhabituel ou même qu’on ne connaît pas du tout, donc on s’assure d’avoir toutes les informations.

Il est important de savoir quoi tester avant de dire que ça ne marche pas

Il n’y a pas nécessairement d’ordre précis pour les différents types de vérifications. On peut commencer par des tests fonctionnels, puis une relecture du code, ou l’inverse : ça peut dépendre des cas. La review peut aussi se faire en plusieurs passes, d’autant plus lorsque des modifications sont apportées en cours de route, et peut comporter des validations partielles (« fonctionnellement ça va, mais le code est à améliorer », ou fichier par fichier, par commit, …).

Aspect fonctionnel

Il s’agit de tester l’utilisation du logiciel et de vérifier que tout fonctionne correctement et comme attendu. On doit donc tester, comme vu plus haut, les nouvelles fonctionnalités, ou vérifier que le bug n’apparaît plus, tout en n’ayant pas de régression, en testant pour cela l’ensemble de la fonctionnalité en question ainsi que celles potentiellement impactées. On peut utiliser pour cela des jeux/fichiers de tests prédéfinis.

Prenons un cas théorique, avec une nouvelle action A. Pour les différentes étapes de test, on peut imaginer une check-list avec quelques points comme ceux-là : (n’hésitez pas à en faire qui correspondent à vos projets) :

  • Est-ce qu’effectuer A fonctionne et est conforme aux attentes ?

  • L'UI est-elle correcte et respectée ?

  • L’état obtenu est-il stable après undo/redo, sauvegarde/rechargement ?

  • A s'intègre-t-elle correctement avec le reste ?

  • Est-ce compatible avec d’anciennes versions ?

  • Est-ce optimisé et performant ? (en termes de fluidité, mémoire…)

  • Tester plusieurs configurations (configurations matérielles, tailles d'écrans, plateformes mobiles….)

  • Tester différents états de l’environnement : habituellement, l’environnement de développement est similaire à celui de production, mais il peut y avoir parfois des différences. Par exemple, dans le cas d’applications Web 3D, le navigateur client peut ne pas avoir WebGL (ou avoir une version différente).

  • Essayer de tout casser : faire un cas d’utilisation peu réaliste, cliquer de partout, avec tous vos boutons de souris, en maintenant la moitié des touches du clavier appuyées,… Vous pourriez être surpris de voir les erreurs qui sortent ! Ce n’est pas si fréquent, mais ça arrive…

(et comme dit plus haut, ce genre d’erreurs se réduisent fréquemment à une petite fausse manip’ qui peut arriver à n’importe qui)

Bien sûr, cette liste n’est pas exhaustive, on peut encore imaginer plein de choses !

Aspect code

Il s’agit de lire et vérifier le code qui a été écrit.

Ouais bon si ça marche c’est l’essentiel, au pire on s’en fout un peu du code…

On peut avoir des vérifications sur l’algorithmique (correction, prise en compte de tous les cas, complexité…), le nommage (variables, fonctions, fichiers… à ne pas sous-estimer car un bon nommage implique de grandes responsabilités une lecture facilitée et un code plus compréhensible)… On peut aussi discuter d’un refactoring, d’éventuels commentaires à ajouter, de la bonne compréhension, d’optimisations, de simplifications… La façon dont le code est écrit a aussi son importance : les conventions du projet (oui car il faut en établir 😜️) doivent être respectées pour garder une harmonie et un code moins confus ; pour simplifier les choses cela peut être fait de manière systématique par un outil automatisé. Par exemple, un outil de mise en forme comme Prettier permet déjà de corriger automatiquement les erreurs de style ; on peut également utiliser un linter (comme ESLint) pour détecter différents types d’erreurs dans le code.

Côté plus meta, on peut noter aussi l’importance de la documentation à tous les niveaux, que ce soit celle du code (commentaires, fonctions, fichiers, algorithmes…) ou la documentation globale (projet, fonctionnalités, utilisation…). Si une personne se plonge dans un fichier de plusieurs centaines de lignes de code sans aucun commentaire, ni aucune fonction documentée, ni document de référence, elle perdra beaucoup de temps…

Quand on se lance dans la compréhension d’un code long, complexe, vieux et non documenté

Internationalisation et accessibilité

Ici, il faut penser à faire des choix d’UI permettant une bonne utilisation du logiciel par n’importe qui.

Par exemple, lorsqu’une application comporte des couleurs, le jeu de couleurs doit être adapté de façon à ce qu’elles soient perçues correctement par tout le monde (voir également cet autre article du blog sur le sujet du daltonisme). Si vous en avez la possibilité, il est préférable qu’une information ne soit pas véhiculée uniquement par la couleur. La perception des contrastes est également à prendre en compte. Des sites ou des outils peuvent aider à mieux analyser cela et à déterminer des jeux de couleurs qui seront correctement perçus dans tous les cas (par exemple, cet article, ce simulateur…).

Lorsqu’une application est disponible en plusieurs langues, on peut vérifier que les traductions (et le texte original) sont présentes, correctes¹, et prennent en compte le contexte. Il faut prendre garde à certaines différences : la longueur des textes et des mots diffère beaucoup d’une langue à une autre ; on peut également avoir des spécificités régionales (au sens large) pour une même langue (par exemple « ground floor » ou « 1st floor » pour « rez-de-chaussée » en anglais britannique ou américain). Par conséquent, la conception de l’UI doit laisser assez d’espace pour les textes, et ceux-ci peuvent être adaptables (défilement,…) (et utiliser Unicode aussi). Ainsi le logiciel est pensé internationalement.

¹ J'insiste sur « correct » : laisser des fautes dans l’UI ça fait pas très pro (et au passage, c’est mieux s’il n’y en a pas dans le code non plus !)

Les questions de langues et de traductions se révèlent toujours complexes

Aspect visuel

Pour ce point, le but est d’observer le rendu de ce qui est affiché, le positionnement des éléments d’UI, la qualité d’affichage, les couleurs… toute la cohérence visuelle de ce qui a été changé ou ajouté.

Une demande indiquant d’ajouter une fonctionnalité peut parfois comporter une demande d’UI précise. La fonctionnalité peut être en parfait état de marche, les demandes concernant l’UI peuvent ne pas être toutes respectées pour autant. Quelques pixels de différence, un élément d’UI un peu décalé… On peut vérifier que les maquettes (s’il y en a) sont respectées, tester le redimensionnement d’écran, différents niveaux de zoom et tailles d’écran, le tout sur plusieurs navigateurs (dans le cas d’une application Web)…

Faire des retours

Tester et faire des retours sont deux choses qui vont ensemble, et la façon de faire des retours dans les merge requests a son importance même si cela peut paraître anodin. Il y a une réelle différence entre des retours faits trop rapidement et des retours plus construits. Lorsque quelque chose ne va pas et qu’on le signale, il s’agit donc d’être plus constructif que « y’a un bug »… C’est un travail d’équipe, et quelques efforts simples permettront de gagner en efficacité.

Le descriptif

Afin de gagner du temps pour les autres développeur(euse)s, un retour consiste à écrire une description précise et détaillée du problème ou de l’amélioration, les logs d’erreurs le cas échéant, et même réaliser une capture vidéo illustrant la marche à suivre pour reproduire l’erreur : c’est souvent très pratique et ça donne lieu à moins d'ambiguïtés. On peut également rappeler ce qui était attendu, à titre de comparaison.

Idéalement, il faut essayer de reproduire le bug de la façon la plus simple possible, en essayant de trouver un cas minimal. Cela facilite par la suite le débug. De plus, c’est un travail d’équipe : si on a une solution, des pistes, des suggestions ou des questions, il ne faut surtout pas hésiter !

Un peu de tact

On peut penser que le but de la review est de repérer les erreurs, mais il s’agit en fait de repérer ce qui va et ce qui ne va pas, pour faire un retour complet. Donc n’oubliez pas de dire aussi ce qui fonctionne correctement ! On prend ainsi en compte l’aspect humain et c’est meilleur pour le moral.

Imaginons une check-list avec 20 points à vérifier, et un seul présente un défaut. C’est moins sympa d’entendre « y’a un bug sur ça » que « points 1 à 19 : ok 😀️ - point 20 : erreur trouvée, voici le détail… ». Un petit effort simple pour éviter d’éventuelles tensions, faire en sorte que le courant passe bien dans l’équipe, éviter qu’il y ait de l’électricité dans l’air [Oui oui il y a bien un combo de jeux de mots…], et rester positif. De plus, les communications indirectes par échanges de messages peuvent paraître, même si ce n’est pas dans les intentions de l’auteur(e), manquer de tact ou trop formelles : un minimum de rédaction, avec supplément gif, peut « détendre » un peu ce type d’échanges.

Pour finir

Nous espérons que ces conseils pour apporter plus de rigueur dans les tests vous seront utiles, qu’ils permettront de couvrir le plus de cas possible et éviter un maximum de soucis.

Bien et beaucoup tester, cela demande sans aucun doute plus d’implication, de temps, de rigueur et d’insistance, mais c’est récompensé. On réduit les bugs au minimum, on évite les erreurs les plus critiques, ce qui donne moins de corrections à faire une fois que le système est déployé en production (« moins », car le zéro bug n’existe pas, même si l’objectif serait de tendre vers cela). Les problèmes ne sont pas réglés après coup mais dès le départ : on évite ainsi au maximum les bugs à la source, on gagne en crédibilité et en confiance, on évite beaucoup de hotfixes et d’éventuelles gênes [oui, les deux derniers liens, c’est du troll.]. Bref, c’est tout bénéf’, et le temps consacré à tout cela est largement rentabilisé. Le système obtenu est fiable, solide, documenté, maintenable, compréhensible, réutilisable, stable,.. et ça arrange tout le monde. (Le seul truc c’est qu’on risque de ne plus pouvoir capturer Mew ou détruire l’étoile de la mort… mais ça, c’est une autre histoire 😉).

Quand on me dit que je suis un bon testeur :

Liens

Tags de
l'article

Cet article n'est pas taggué.

Catégories de l'article

Méthodologie Développement

Commentaires

Il n'y a actuellement aucun commentaire. Soyez le premier !

Articles liés