Tests assistés par IA : utiliser les tests unitaires comme garde-fou pour le code généré par l'IA
Categories:
Les tests assistés par IA ne résolvent pas le problème de la couverture des tests, mais le problème de ma confiance dans le code généré par l’IA. L’IA peut générer un grand bloc de code en trois minutes, tandis qu’il faut deux heures à une personne pour l’examiner entièrement ; trois minutes pour générer, deux heures pour examiner n’est pas exagéré. Ma solution est de changer d’objet de revue : au lieu de lire le code ligne par ligne, on lit le document de test, puis on met en place une barrière de tests unitaires dans l’environnement de développement local, pour que les cas en échec me surveillent la négligence et l’erreur de l’IA.
Le petit bug est le principal champ de bataille de l’aide au test IA
Un bug se divise en deux catégories : le bug facile à résoudre et le bug difficile à résoudre.
Le bug difficile à résoudre provient généralement d’une définition de l’architecture ou du schéma initial, et évolue progressivement en dette historique, qui peut se propager à tout le système ; il peut aussi provenir de tiers, comme une bibliothèque tierce, du système d’exploitation, du compilateur, et nécessite des connaissances plus spécialisées et une exploration approfondie pour être résolu. Ce type de problème complexe n’est pas traité dans cet article.
Cet article se concentre sur les petits bugs dans le cycle de vie du produit, et utilise l’aide de l’IA à la création de cas de test pour résoudre les problèmes suivants :
- Les bugs qui peuvent être rapidement détectés
- Les bogues de faible niveau qui ne nécessitent qu’une petite modification pour être corrigés
- Les nouveaux bogues introduits par une modification
- Les erreurs faites sous pression lors d’une modification urgente
- Le coût et l’efficacité du test ciblé
La plupart des bugs sont petits, mais un petit bug peut aussi provoquer de graves problèmes. Résoudre les petits bugs localement peut permettre d’économiser beaucoup d’énergie par la suite.
Changer d’objet de revue de code en document de test
J’ai essayé une idée la plus audacieuse : laisser l’IA finaliser entièrement une exigence, sans examiner du tout le code qu’elle génère, comme dans le mode Agent de Copilot. J’ai abandonné. La raison fondamentale était le manque de certitude. Pour un programme binaire logique et rigoureux, un manque de certitude signifiait perdre la confiance en amont et en aval, et la confiance des clients. J’ai donc appris à réfléchir au vibe coding : pour le code généré par l’IA, il faut toujours avoir une certitude intérieure.
Le problème vient du coût de revue. Parfois, l’IA génère ou modifie trop de code : trois minutes pour générer, deux heures pour examiner n’est pas exagéré. La charge de travail des tests est généralement supérieure à celle du code métier, et elle n’est pas toujours incluse dans l’évaluation des livraisons de R&D, ce qui est l’une des raisons pour lesquelles beaucoup ne veulent pas écrire de tests. Pourtant, l’assistant IA est bien plus intelligent que moi, c’est un fait. Plutôt que de résister, autant lui mettre une garde.
Changer d’objet de revue de code en document de test est la clé pour contourner cette impasse. Contrairement au code métier, les tests générés par l’IA décrivent généralement un scénario très précis, facile à examiner et à valider. De nombreux cas n’ont même pas besoin d’être lus ligne par ligne ; on attend simplement qu’il échoue pour regarder plus en détail. Affirmer qu’on ne réexamine jamais le code de l’IA serait exagéré, mais grâce à un document de test complet, on peut prouver que toutes les lignes de code n’ont pas besoin d’un examen humain.
---
title: De la revue du code au document de test
---
flowchart TB
A[IA génère une grande quantité de code] e1@--> B[Coût de revue ligne par ligne élevé]
B e2@--> C{Qu'est-ce que l'on révise ?}
C e3@-->|Code| D[Deux heures de revue, manque de certitude]
C e4@-->|Document de test| E[Facile à revoir, validation rapide]
E e5@--> F[Barrière de tests unitaires]
F e6@--> G[Le cas en échec expose le problème]
classDef risk fill:#FFF3E0,stroke:#E65100,color:#E65100;
classDef ok fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20;
classDef animate stroke:#EF6C00,stroke-width:2px,stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
class D risk;
class E,F,G ok;
class e1,e2,e3,e4,e5,e6 animate;Fondement méthodologique : Six Sigma et DMAIC
Six Sigma est une méthode de qualité fondée sur les données, son objectif principal est de réduire la variabilité du processus et de limiter le taux de défauts à 3,4 par million, c’est-à-dire le niveau 6σ1.
Elle propose un processus exécutable appelé DMAIC :
- Define (Définir) : analyse des exigences, transformation en modèle de données, conception des structures de données et des interfaces
- Measure (Mesurer) : surveillance des cas, couverture, performance, sécurité
- Analyze (Analyser) : analyse des cas et du code métier
- Improve (Améliorer) : amélioration des cas et du code métier
- Control (Contrôler) : porte de test automatisée
Les outils techniques associés comprennent le linter, les contrôles statiques, les contrôles dynamiques et les frameworks de test. Les indicateurs de qualité de code que l’on peut suivre sont nombreux, par exemple les indicateurs de complexité (complexité cyclomatique, complexité cognitive, profondeur des niveaux de nesting, couplage de classes), les indicateurs de qualité (taux de duplication, longueur de fonction, nombres magiques), ainsi que les indicateurs de test (couverture des tests unitaires, taux de réussite des tests d’intégration, validité des cas, couverture des exceptions).
La méthode et les outils peuvent aider à découvrir et à résoudre les problèmes, mais à un coût qui peut, à certains niveaux, augmenter les coûts de livraison et réduire l’efficacité. Avec des ressources limitées, il faut choisir une méthode bénéfique à la fois à court, moyen et long terme. Mon choix est d’utiliser les tests unitaires comme outil auxiliaire dans le processus de développement.
Grâce à l’IA dans les conversions entre les langages de différents rôles, les développeurs peuvent fusionner les avantages du TDD, du BDD et de l’ATDD, générer rapidement des tableaux et codes de cas, et améliorer la qualité de livraison.
TDD/BDD/ATDD et code testable
Les différences entre les trois approches de test piloté sont les suivantes :
| Dimension | TDD | BDD | ATDD |
|---|---|---|---|
| Source du driver | Tests unitaires | Scénario comportemental utilisateur | Conditions d’acceptation |
| Participants | Développeur | Métier + développement | Métier + tests + développement |
| Niveau de test | Tests unitaires | Tests d’intégration / de bout en bout | Tests d’acceptation |
| Exemple d’outil | JUnit, pytest | Cucumber, SpecFlow | FitNesse, Robot Framework |
| Sortie typique | Rapport de couverture du code | Document de scénario utilisateur exécutable | Taux de réussite des tests d’acceptation |
| Langue | assert add(2,3) == 5 | Scénario en langage naturel (Given-When-Then) | Langue métier (tableau de conditions d’acceptation) |
Un code testable repose sur quatre principes fondamentaux : une seule responsabilité (chaque fonction ne fait qu’une chose), l’injection de dépendances (les dépendances sont passées par paramètres), l’isolation des interfaces (les types d’entrée et de sortie sont clairs), et l’observabilité de l’état (fournir une interface de requête pour valider l’état interne).
Dans les invites destinées à l’IA, je suggère de rédiger directement extensible, une seule responsabilité, injection de dépendances. J’ai essayé de faire contrôler à l’IA la longueur des fonctions et des fichiers, et son adhésion était médiocre ; mais en passant à une seule responsabilité et à l’injection de dépendances, le code généré correspondait nettement mieux aux attentes. L’optimisation prématurée est une source de tous les maux ; lors de la génération de structures de données et d’interfaces de base, il suffit de suggérer à l’IA de privilégier l’extensibilité.
Exemple de code facile à tester :
// user.h
typedef struct {
int id;
char name[50];
int age;
} User;
// Interface d'accès à la mémoire
typedef struct {
User* (*get_user)(int id);
int (*save_user)(const User* user);
} UserStorage;
// Créer un utilisateur (injection de dépendance du stockage)
User* create_user(const char* name, int age, UserStorage* storage) {
if(strlen(name) >= 50 || age <= 0) return NULL;
User* new_user = malloc(sizeof(User));
new_user->id = generate_user_id();
strncpy(new_user->name, name, 49);
new_user->name[49] = '\0';
new_user->age = age;
if(storage->save_user(new_user) != 0) {
free(new_user);
return NULL;
}
return new_user;
}
Exemple de code difficile à tester :
// Opération de base de données fortement couplée
void save_user_to_db(User* user) {
MYSQL conn;
mysql_init(&conn);
if(!mysql_real_connect(&conn, "localhost", "root", "pass", "test", 0, NULL, 0)) {
log_error("DB connection failed"); // Dépendance de journalisation difficile à tester
exit(1); // Sortir directement impacte l'exécution des tests
}
// Concaténer SQL et écrire dans la base de données
}
Un bon code est silencieux, un mauvais code est très visible. Les fonctions faciles à tester ont des valeurs de retour claires, une gestion des erreurs standard, des paramètres d’entrée clairs, une isolation des effets de bord et la prise en charge de l’injection de dépendances.
Le code donné par l’IA n’est parfois pas une meilleure pratique, et il y a aussi des problèmes de style. Il peut souvent résoudre le problème, mais un projet réel n’est pas une affaire à un coup. Il faut considérer la maintenance ultérieure. Lorsqu’on demande à l’IA de générer du code, il faut également insister sur la lisibilité d’abord, l’extensibilité, une seule responsabilité, l’injection de dépendances. Le problème de style ne nécessite pas plus de discussion, on peut se référer au guide de style de Google2.
Pièges et compromis du développement réel
Lorsqu’une exigence transite entre les différents rôles, elle peut passer par plusieurs conversions de langue, et chaque conversion peut produire des écarts de formulation. Par exemple, ce qu’un client dit « consulter de manière centralisée » peut être compris par le backend comme « renvoyer une seule interface » sans pagination. Grâce à l’IA pour générer des modèles de langage pour chaque rôle, on peut réduire les pertes d’information liées à la conversion manuelle.
Un manque de réflexion est souvent la source de nombreux bogues. Grâce à l’IA pour concevoir des scénarios de cas, il peut non seulement accomplir la tâche attendue, mais aussi donner des idées inattendues et combler les détails que l’émetteur du message n’avait pas pris en compte. Dans chaque conversion entre les rôles, on peut dialoguer avec l’IA pour améliorer les lacunes. Markdown est le langage le plus facile pour l’IA à comprendre, la saisie et la sortie doivent donc idéalement utiliser ce format.
Le code de test comporte plusieurs points qui doivent être notés : isolation des cas, exactitude des dépendances simulées, validité des assertions, valeurs limites, gestion des exceptions, performance, concurrence. L’IA peut être paresseuse et ne génère pas un ensemble de cas couvrant tous les scénarios en une seule fois. Générer tous les codes de test en une seule fois est une exigence plus élevée pour le modèle ; on peut procéder par plusieurs générations.
En prenant l’exemple du modèle Claude dans Cursor, il traite assez bien les points importants liés au code de test, mais oublie souvent l’isolation des cas et les scénarios d’exception. Ajouter une règle utilisateur peut un peu réduire la paresse : lors de la génération de cas de test, faire attention au document de test et à l’isolation des cas. Cela dit, il ne génère toujours pas tous les scénarios en une fois ; les scénarios normaux, exceptionnels, valeurs limites, performance et concurrence doivent être ajoutés par plusieurs échanges.
Un flux de travail AI d’aide au test réalisable
En combinant le document de conception, on peut assembler une chaîne allant du scénario au cas :
---
title: Flux de travail d'aide au test IA
---
flowchart TB
A[Document de conception] e1@--> B[Générer des scénarios de test d'acceptation]
B e2@--> C[Générer les structures de données et les interfaces]
C e3@--> D[Générer le tableau de cas côté terminal]
D e4@--> E[Générer le code de test]
D e5@--> F[Générer le code métier]
E e6@--> G[Porte locale : les cas passent]
F e7@--> G
G e8@--> H{Un cas a échoué ?}
H e9@-->|Oui| E
H e10@-->|Non| I[Faire la demande de test]
classDef input fill:#E3F2FD,stroke:#1565C0,color:#0D47A1;
classDef gen fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20;
classDef gate fill:#FFF3E0,stroke:#E65100,color:#E65100;
classDef animate stroke:#EF6C00,stroke-width:2px,stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
class A input;
class B,C,D,E,F gen;
class G,H,I gate;
class e1,e2,e3,e4,e5,e6,e7,e8,e9,e10 animate;Les invites pour chaque étape peuvent être organisées ainsi : générer des scénarios de test d’acceptation à partir du document de conception produit ; générer les structures de données et les interfaces à partir des scénarios de test ; générer le tableau de cas côté terminal à partir des scénarios et des interfaces ; générer le code de test à partir des interfaces et du tableau de cas ; générer le code métier à partir des scénarios de test et s’assurer que les cas passent.
Il y a un risque ici : l’IA peut aussi modifier le code métier en fonction du code de test, ou bien modifier le code de test en retour. S’il y a des erreurs des deux côtés, l’IA peut être confuse, et à mesure que le dialogue progresse, le code de test et le code métier deviennent de plus en plus désordonnés ; il faut alors intervenir manuellement.
Une invite trop ambitieuse est un contre-exemple : elle demande à l’IA de couvrir en une seule fois les scénarios normal, exceptionnel, valeurs limites, concurrence et de générer du code. Ce type de demande est facile à interrompre. La bonne approche consiste à découper les scénarios dans Cursor et à générer par lots via plusieurs requêtes. Les cas de performance nécessitent beaucoup de données ; ils doivent être placés à l’étape des tests système, d’abord demander à l’IA d’écrire un générateur de données aléatoires, puis construire les cas avec les données générées.
Les tests d’intégration nécessitent de construire un environnement de test. Certains cas dépendent d’outils externes, par exemple la micro-isolation dépend d’iptables et d’ipset ; on peut utiliser Docker pour construire le conteneur de test. La règle est la suivante : on fait tester par l’humain, puis on fait implémenter par l’IA les mêmes étapes automatisées. Pour les tests de sécurité, on dépend actuellement surtout de l’expérience et de l’analyse, et il n’y a pas encore de bonnes pratiques d’aide à l’IA.
Conclusion
Ce flux de travail produira, par rapport à un flux sans IA, plus de documents et de code de test ; il est difficile de tout revoir mot à mot. Mais les documents sont plus faciles à examiner que le code, en particulier les tableaux de cas, qui jouent un rôle précis dans la génération des cas. Dans le développement réel, je ne révise parfois que les cas en échec, et je confirme le problème en débogant les cas en échec.
Les axes d’amélioration sont trois : les sous-processus de la phase de conception présentent des chevauchements ; les scénarios de test d’acceptation peuvent dépendre des structures de données et des interfaces ; le tableau de cas côté terminal doit être suivi par les équipes de R&D ; avant de soumettre à test, il faut régénérer des scénarios de test détaillés incluant les détails du code métier.
Le gain final ne réside pas dans le chiffre de la couverture des tests, mais dans la confiance : grâce à la porte de test unitaire locale, je peux enfin avoir une certitude intérieure concernant le code généré par l’IA, au lieu de livrer sans réflexion le code « vibing ».