Notre blogue | Nexapp

IA : comment éviter les tests logiciels fragiles et inutiles

Rédigé par Alexandre Rivest | Jan 26, 2026 7:05:05 PM

L'intelligence artificielle révolutionne notre façon de travailler en informatique. Elle change nos pratiques, nos manières de penser, et nous permet d'aller beaucoup plus vite sur de nombreuses tâches. Mais j'observe une tendance préoccupante : de plus en plus de développeurs et développeuses qui ne sont pas habitués à écrire des tests les génèrent automatiquement avec l'IA. 

À première vue, c'est le rêve. Plus besoin d'écrire tout ce code répétitif, plus besoin de se creuser la tête pour trouver tous les cas limites. Mais au fil de mes années en développement logiciel, j'ai appris une chose : la vitesse de production n'est pas synonyme de qualité. On peut produire beaucoup de tests très vite, mais est-ce pour autant qu'on produit de bons tests ?

L'IA sait comment écrire des tests. Mais écrire de bons tests, c'est une autre histoire.

Ironiquement, avec l'IA, on n'a plus vraiment d'excuse pour ne pas écrire de tests. L'IA peut nous aider à les générer, à trouver des cas limites, à accélérer le travail répétitif. Mais voici le piège : les tests sont devenus encore plus critiques qu'avant. Pourquoi ? Parce que l'IA elle-même introduit des risques. Elle va vite, elle est productive, mais elle peut facilement casser des choses par accident ; un peu comme un développeur junior enthousiaste. On aurait donc besoin de tests encore plus solides pour nous protéger de ces erreurs. Le problème ? Les tests que l'IA génère par défaut ne sont pas bons.

J'ai récemment vécu une situation qui illustre parfaitement ce problème. Un développeur chez un client allait écrire ses premiers tests dans l'application! C'est un projet où les tests ont fait leur arrivée tout récemment. Enthousiaste, il a utilisé l'intelligence artificielle pour générer ses premiers tests. Le résultat? Des tests qui valident ligne par ligne l'implémentation, avec des mocks partout, extrêmement fragiles au moindre changement de code.

 

Les tests : un outil fondamental, pas une corvée

Pour comprendre pourquoi c'est problématique, revenons aux fondamentaux. Pourquoi écrivons-nous des tests ? Ce n'est pas juste pour faire plaisir au QA ou pour avoir une belle couverture de code à montrer au client. Ce n'est pas parce que c'est tendance en 2026!

Les tests sont notre filet de sécurité. Dans le développement logiciel, la seule constante est le changement. Les exigences changent, les technologies évoluent, et nous devons constamment adapter notre code. Sans tests, chaque modification est un saut dans l'inconnu.

Quand on développe une application complexe, il devient facile de faire des changements qui brisent accidentellement d'autres parties de l'application. Ailleurs dans le code, ou même directement dans le code qu'on est en train de modifier. Même si on applique des patterns d'architecture avancés pour rendre le code modulaire et minimiser les risques, l'architecture elle-même peut devenir défaillante. C'est à ce moment que les tests peuvent vraiment briller dans leur rôle de protecteur.

 

Tester les comportements, pas l'implémentation

C'est justement pour cette raison que les tests doivent se concentrer sur les comportements de l'application, et non sur les détails d'implémentation. Un test lié à l'implémentation, qui vérifie qu'une méthode privée est appelée, qu'un certain nombre de lignes s'exécute, ou qu'une variable interne a telle valeur, devient un frein. Dès qu'on refactorise le code, même sans changer le comportement visible, ces tests cassent.

Un test orienté comportement, lui, reste valide tant que l'application fait ce qu'elle est censée faire. Il décrit ce que le système fait, pas comment il le fait. C'est la différence entre « l'utilisateur peut se connecter avec un mot de passe valide » et « la méthode validatePasssword retourne true après avoir appelé hasService.compore ».

En développement logiciel, on parle de tests en boîte blanche (qui connaissent l'intérieur du système) et en boîte noire (qui ne voient que les entrées et sorties). L'approche orientée comportement, c'est la boîte noire : « Si je fournis X, est-ce que j'obtiens Y ? ». Sans se soucier de comment le système arrive à ce résultat.

 

Les limites de l'IA dans la génération de tests

Maintenant qu'on comprend la distinction entre tester le comportement et tester l'implémentation, regardons ce que l'IA fait concrètement quand on lui demande de générer des tests.

Voici ce que j'ai remarqué avec le temps : l'IA a tendance à écrire des tests en boîte blanche. Elle va :

  1. Prendre pour acquis toute l'information à l'intérieur de la classe ou du composant
  2. Mocker tout ce qui n'est pas dans le fichier testé
  3. Assumer qu'elle sait comment tout fonctionne

Résultat : des tests qui testent ligne par ligne le code, exactement ce qu'on veut éviter. On veut des tests agnostiques de l'implémentation, car ils sont beaucoup moins fragiles.

Voici un exemple concret de ce que l'IA génère typiquement :


// ❌ Test généré par l'IA - couplé à l'implémentation


test('login calls authService.validatePassword and returns user', async () => {

  const mockAuthService = {

    validatePassword: jest.fn().mockResolvedValue(true),

    generateToken: jest.fn().mockReturnValue('token-123'),

  };

  const mockUserRepository = {

    findByEmail: jest.fn().mockResolvedValue({ id: 1, email: 'test@test.com' }),

  };


  const result = await login('test@test.com', 'password', mockAuthService, mockUserRepository);


  expect(mockUserRepository.findByEmail).toHaveBeenCalledWith('test@test.com');

  expect(mockAuthService.validatePassword).toHaveBeenCalled();

  expect(mockAuthService.generateToken).toHaveBeenCalledWith({ id: 1 });

  expect(result.token).toBe('token-123');

});


Ce test vérifie comment le code fonctionne : quelles méthodes sont appelées, dans quel ordre, avec quels paramètres. Si demain on refactorise login pour utiliser un seul service au lieu de deux, ce test casse, même si le comportement reste identique.

Comparons avec un test orienté comportement :


//
Test orienté comportement


test('given valid credentials, when logging in, should return authentication token', async () => {

  // Given: un utilisateur existant avec des identifiants valides

  await createUser({ email: 'user@example.com', password: 'valid-password' });


  // When: l'utilisateur se connecte

  const result = await login('user@example.com', 'valid-password');


  // Then: il reçoit un token d'authentification

  expect(result.token).toBeDefined();

  expect(result.success).toBe(true);

});

 

Ce test décrit ce que le système fait, pas comment. On peut refactoriser l'implémentation autant qu'on veut : tant que l'utilisateur peut se connecter et recevoir un token, le test reste vert.

 

L'exception : les tests de caractérisation

Lorsque vous devez modifier un système legacy sans tests, il est normal de ne pas connaître l'ensemble des fonctionnalités qui seront affectées. Les tests de caractérisation capturent le comportement actuel du système, quel qu'il soit, pour créer un filet de sécurité avant le refactoring.

Dans ce contexte spécifique, l'IA peut rapidement générer une couverture de tests qui documente l'état actuel du code, sans les biais qu'un humain pourrait avoir sur ce qui "devrait" se passer. C'est l'un des rares cas où les tests en boîte blanche sont non seulement acceptables, mais souhaitables.

Attention : Ces tests restent temporaires. Une fois le refactoring terminé, ils doivent être remplacés par de vrais tests de comportement.

 

Où l'IA brille vraiment

Au-delà des tests de caractérisation, l'IA excelle pour les tâches fastidieuses où le développeur garde le contrôle :

  • Refactoring de tests existants : Éliminer la duplication, regrouper le code de setup dans des fonctions réutilisables, améliorer la lisibilité.
  • Génération de données : Créer des jeux de données réalistes, des mocks d'API basés sur des interfaces TypeScript.
  • Cas limites : Suggérer des cas limites (edge cases) qu'on aurait pu oublier.

Mais pour créer les tests en soi sur du nouveau code, s'il n'a pas d'exemple ou de guide sur comment écrire de bons tests, l'IA ira par défaut dans la création de tests sans valeur.

Et c'est là le problème. Pour fournir ce guide à l'IA, le développeur doit lui-même savoir à quoi ressemble un bon test. Et même au-delà : chaque test généré doit être revu. Si on ne sait pas reconnaître un bon test d'un mauvais, comment peut-on faire la revue du code que l'IA produit? On se retrouve à valider aveuglément du code qu'on ne comprend pas vraiment, ce qui est exactement l'inverse de ce qu'on cherche.

 

Apprendre à écrire de bons tests avant d'automatiser

Revenons à la situation chez le client où le développeur avait commencé à générer des tests avec l'IA. Pour sa tâche, j'ai décidé de revoir les tests avec lui pour montrer comment les structurer correctement.

 

Étape 1 : Écrire uniquement les noms de tests

La première étape, c'est d'écrire uniquement les noms des tests. Pas l'implémentation. Juste les noms.

On a regardé le service ensemble, la fonctionnalité qu'il avait implémentée, et je lui ai demandé de réfléchir : "C'est quoi les comportements possibles d'un point de vue utilisateur?"

Cette question force à penser en termes de comportements, pas d'implémentation.

Pour un exemple de page de connexion, ça pourrait donner :

  • "Lorsque l'utilisateur entre un mot de passe valide, il accède à son tableau de bord"
  • "Lorsque l'utilisateur entre un mot de passe invalide, un message d'erreur s'affiche"
  • "Lorsque l'utilisateur a trop de tentatives échouées, son compte est verrouillé"

Ces phrases mettent l'accent sur le comportement désiré. C'est l'étape où il faut vraiment réfléchir sérieusement aux différents cas possibles.

Astuce : Ces cas de comportement font souvent partie des critères d'acceptation des tâches. Les QA, quand ils planifient leurs tests manuels, ont probablement déjà ces réflexions. Impliquez-les! Le Product Owner aussi peut vous aider à définir ces cas. On n'a pas la connaissance ultime du produit. Profitez de l'expertise de chacun.

Quelque chose de super important si le code a déjà été implémenté avant de définir les tests : ne pas essayer de se fier principalement à l'implémentation pour les définir. Il y a un risque de trop s'appuyer sur le "comment" au lieu du "quoi". Le point de départ doit toujours être : "C'est quoi le comportement attendu du point de vue de l'utilisateur?"

 

Étape 2 : Structurer le squelette avec des commentaires

Une fois les noms de tests écrits (et idéalement validés avec l'équipe), on passe à la structure. Mais pas directement au code!

La deuxième étape, c'est d'écrire le squelette du test avec des commentaires pour chaque section.

Par exemple, pour le test du mot de passe invalide :


test('given
an invalid password, when logging in, should display error message', () => {


  // Given: simuler un utilisateur qui entre un mauvais mot de passe


  // When: soumettre le formulaire de connexion


  // Then: vérifier que le message d'erreur s'affiche

});

Le format given-when-then du nom nous guide naturellement pour structurer le test :

  • Given : Qu'est-ce qu'il faut préparer?
  • When : Quelle action déclenche le comportement?
  • Then : Quelle est l'assertion attendue?

Cette étape aide à se poser les bonnes questions sans encore plonger dans l'implémentation. On fait une ébauche de ce que ça devrait faire.

Conseil : Ces différentes sections doivent vraiment refléter le nom du test. Si ce n'est pas le cas, c'est que vous êtes probablement trop proche de l'implémentation.

 

Étape 3 : Implémenter le test

Maintenant qu'on a notre structure, on peut implémenter chaque section. On peut ajouter des commentaires plus détaillés si nécessaire avant d'écrire le code :


test('given
an invalid password, when logging in, should display error message', () => {


  // Given: simuler un utilisateur qui entre un mauvais mot de passe

  // - Afficher le formulaire de connexion

  // - Préparer le refus de connexion


  // When: soumettre le formulaire de connexion

  // - Remplir les champs email et mot de passe

  // - Cliquer sur le bouton de connexion


  // Then: vérifier que le message d'erreur s'affiche

  // - Chercher le message d'erreur dans le DOM

  // - Valider que l'utilisateur n'est pas redirigé

});


C'est à ce moment qu'on réfléchit aux détails techniques : fixtures, mocks nécessaires (le moins possible!), sélecteurs DOM, etc.

Cette approche en trois étapes met l'accent sur le genre de tests qui amènent réellement de la valeur. Elle s'inspire du BDD (Behavior-Driven Development), qui structure les tests autour des comportements attendus plutôt que des détails d'implémentation. Et si on applique ces étapes avant d'écrire le code, on fait du TDD (Test-Driven Development). Les tests guident alors le design plutôt que de le documenter après coup.

Pour en savoir plus :  BDD (Behaviour-Driven Development) et  TDD (Test-Driven Development)

 

Utiliser l'IA comme un copilote, pas comme un pilote

Alors, faut-il bannir l'IA? Absolument pas. Mais il faut faire la distinction entre "l'IA qui fait le travail à votre place" et "l'IA qui vous aide à aller plus vite". Le développeur doit rester aux commandes. Il doit comprendre ce qu'est un bon test et avoir les compétences pour l'écrire lui-même. Sinon, comment évaluer si le test généré est bon? Qu'est-ce qui devrait changer? Pourquoi ce mock est-il problématique?

Le développeur définit le quoi, l'IA accélère le comment.

 

Conclusion

L'IA est un formidable accélérateur, mais elle ne remplace pas la compréhension humaine des produits technologiques.

Automatiser les tests avec l'IA, c'est un peu comme utiliser un GPS. C'est pratique pour aller vite, mais si vous ne regardez pas la route, vous finirez dans le décor. Le développeur reste le pilote. C'est vous qui connaissez le contexte, les enjeux métier et ce qui compte vraiment pour vos utilisateurs.

Avant de déléguer à l'IA, maîtrisez les bases : pensez en comportements, structurez vos tests, évaluez ce qui est produit. Une fois cette compétence acquise, l'IA devient une alliée redoutable.

Un test n'est pas là pour prouver que le code fonctionne. Il est là pour permettre au code de changer sans peur. Et ça, aucune IA ne peut le garantir à votre place.