Automatisation et Qualité de Code
- Fixer les Règles de Qualité de Code Dès le Début
- Les Tests Statiques : Détecter les Problèmes Sans Exécuter le Code
- Les Familles de tests et leur automatisation
- Le fil rouge ? Quel fil rouge ?
En tant que vibe coder, qu’on construise son application en échangeant avec Chat GPT, ou qu’on délègue à Claude ou Opencode, deux points tendent à passer à la trappe : la qualité et la sécurité du code. On a tendance à se focaliser sur les fonctionnalités, pour être capable de rapidement obtenir un résultat exploitable … Mais rapidement, des failles de sécurité apparaissent : les données sensibles ne sont pas protégées, le code est difficile à maintenir car il ne suit pas de règles strictes …
Lors de mes premiers tests avec IA (notamment avec Opencode), de vieux réflexes m’ont bien aidé à préserver une qualité de code à peu près potable, sans effort : ma pipeline de CI.
Fixer les Règles de Qualité de Code Dès le Début
Notre code est comme une maison : si nous construisons sans plan, ça va s’écrouler. C’est pour ça qu’il faut fixer des règles de qualité très tôt, avant que tout parte en vrille. En pratique, ça veut dire utiliser des outils qui analysent notre code pour repérer les erreurs de style, les bugs potentiels ou les problèmes de typage. Cela nous évite la dette technique : l’ensemble des petites faiblesses qui s’accumulent et rendent notre projet ingérable plus tard.
En Python, on utilise souvent :
- Pylint : C’est un linter qui vérifie le style de code (indentation, noms de variables, etc.) et détecte des erreurs logiques. Il donne une note sur 10 à ton code. L’équivalent en Javascript est ESLint.
- Mypy : Pour le typage statique. Python est dynamique (pas besoin de déclarer les types), mais mypy ajoute des checks pour éviter les erreurs comme passer une chaîne à une fonction qui attend un nombre. En Javascript, une solution simple et efficace est de coder en Typescript : tsc (qui convertit le code Typescript en Javascript) va naturellement lever les erreurs à la conversion du code.
- D’autres comme Black (pour formater automatiquement) ou Flake8 (linter général). Un équivalent Javascript sera par exemple Prettier.
Ces outils s’intègrent facilement dans une pipeline CI, comme dans un fichier .gitlab-ci.yml où nous définissons des jobs pour lancer pylint et mypy sur chaque push ou Merge Request. Par exemple, un job pourrait être : installer les dépendances, puis pylint --rcfile=.pylintrc *.
Depuis la pipeline, les outils comme pylint vont bloquer la Merge Request si le code n’est pas au top, et afficher des logs … Qu’on pourra donner à notre IA pour aller chercher et corriger les défauts du code.
Les Tests Statiques : Détecter les Problèmes Sans Exécuter le Code
Un code généré ou développé à la va-vite aura de gros risques de contenir des failles. L’ une des failles les plus connues sont les failles permettant l’injection SQL (un pirate peut faire exécuter du code SQL sur notre base de données et extraire des informations sensibles, voire obtenir un accès permanent). Une mauvaise maîtrise de son code peut également aboutir à l’exposition de mots de passe et autres données sensibles qui permettraient à un attaquant de prendre le contrôle de notre infrastructure et de notre application … Corriger ces failles implique de revoir et modifier notre code, ou bien utiliser des outils comme un “Password Vault” pour éviter d’exposer les données sensibles.
Même si notre code est propre, un autre type de faille peut affaiblir notre application : les CVE (Common Vulnerabilities and Exposures). Ce sont des failles de sécurité connues et répertoriées qui peuvent affecter une dépendance, c’est-à-dire une bibliothèque utilisée pour notre application (comme Flask ou Express). Dans ces conditions, la solution la plus courante consiste à mettre à jour nos dépendances, ou bien carrément à la remplacer par une alternative plus fiable.
Les tests statiques sont comme un scanner qui lit notre code sans le lancer. Ils permettent de repérer les vulnérabilités ou des patterns dangereux tôt, sans attendre que ça crash en prod. En Python, un outil star est Bandit. En Javascript, on peut utiliser une extension d’ESLint appelée eslint-plugin-security (simple mais génère parfois des faux-positifs), ou bien Semgrep ou njsscan.
Ces outils vont scanner le code pour vérifier toute exposition de données sensibles, comme des mots de passe en clair, du code permettant des injections SQL, voire même identifier les imports risqués. Ils vont alors générer des rapports, souvent avec des niveaux de sévérité (low, medium, high), qui pourront être exploités par notre IA.
À noter qu’avec Gitlab CI et Github Action, il existe également des templates qui peuvent être intégrés aux pipelines pour ajouter une couche supplémentaire aux contrôles de sécurité.
Les Familles de tests et leur automatisation
Les tests, c’est le filet de sécurité de ton code. Il y a de nombreux types et niveaux de test.
- Tests unitaires : Ils vérifient des petites parties isolées (une fonction, une classe). Ex : Tester si une fonction additionne bien 2 + 2 = 4.
- Tests fonctionnels : Ils valident le comportement global, comme une fonctionnalité entière (ex : un formulaire de login), sans se préoccuper du comportement interne.
- Tests d’intégration : Ces tests valident l’intégration d’une application dans son environnement, typiquement avec base de données, API tierces, etc.
- Tests non-fonctionnels : Ce type de tests se concentrent sur d’autres aspects, comme la performance, l’utilisabilité, la fiabilité et la maintenabilité.
- Tests de non-régression : Ceux-ci permettent de vérifier que les changements n’ont pas cassé l’existant.
- Tests d’acceptation : Ceux-ci sont du niveau produit, dans la mesure où ils valident que l’application et ses fonctionnalités répondent aux besoins des utilisateurs.
- etc.
NB : Il existe encore bien plus de niveaux et familles de tests, mais ceux-ci suffiront amplement pour notre cas. Si vous souhaitez aller plus loin, je vous invite à vous renseigner sur l’ISTQB, qui définit encore bien d’autres catégories, et des démarches qualité beaucoup plus abouties que ce que nous voyons ici.
Les tests fonctionnels, non-fonctionnels, d’intégration, de non-régression ou d’acceptation sont souvent des tests lourds à exécuter : ils nécessitent des mocks (qui servent à simuler les dépendances externes de notre programme), des bases de données, etc. Ils prennent également du temps à l’exécution. Comme nous l’avons vu dans R2PL (cf. Lien substack / lien Nostr), une bonne solution est de les intégrer dans une pipeline de CI qui sera exécutée moins fréquemment : chaque nuit si possible, sinon chaque week-end. Au retour, on va alors investiguer les résultats et appliquer les correctifs adéquats (ou alors, notre IA va s’en charger ;-))
Les tests unitaires, quant à eux, peuvent … et doivent être exécutés à chaque Merge Request, pour garantir la qualité de code. L’idéal étant de viser une couverture du code à 100%. Néanmoins, ils requièrent de la prudence : ce n’est pas parce qu’une fonction valide 100% de ses tests que l’application validera le comportement attendu.
Là aussi, chaque langage propose des outils pour l’exécution des tests : pytest avec Python, Jest pour Javascript ou Typescript. À noter que des outils plus poussés, comme Selenium, vont intervenir pour des tests d’intégration ou des tests fonctionnels plus poussés. Par exemple, on peut simuler un navigateur avec un utilisateur naviguant dans l’interface de notre application.
Idéalement, il vaut mieux concevoir les tests en amont (soit pendant le sprint, soit avant). Cela permet d’anticiper de traiter et de régler les problèmes de sécurité et de qualité.
Le fil rouge ? Quel fil rouge ?
Comme nous l’avons vu, quelques règles de base reviennent à chaque fois.
- Anticiper : Intégrer les outils pour la sécurité le plus tôt possible, concevoir et intégrer ses tests dès qu’on conçoit notre solution, … Cela nous évite d’avoir à traiter de grands volumes d’erreurs ou de faille en une fois, mais plutôt de les traiter au fur et à mesure. Le TDD, ou “Test Driven Development”, est justement un grand classique du développement : il consiste à s’appuyer sur l’exécution des tests pour valider que notre code est prêt et opérationnel.
- Automatiser : L’exécution des outils et des tests sur le code est chronophage. Leur automatisation via la pipeline de CI et leur intégration avec l’IA permet de garantir le maximum de qualité possible avec le moins d’effort et de coût en temps possible.
- Contrôle : L’IA peut nous aider autant qu’elle veut, la conception et la revue de cette conception doivent passer par nous, l’être humain qui a eu une idée merveilleuse d’application. Nous devons garder la main sur la conception et sur les tests finaux de l’application, et garder un œil sur le code produit.
Avec ces principes, et ce que j’ai mis en place sur mon .gitlab-ci.yml, j’ai demandé à Opencode de scanner les erreurs remontées sur ma pipeline de CI. Il a alors naturellement corrigé le code qu’il avait généré pour valider tous les jobs.
Write a comment