MAISON CODE .
/ Tech · Testing · QA · Jest · Best Practices

Tests unitaires : le filet de sécurité, alias comment dormir la nuit

La couverture du code à 100 % est une mesure vaniteuse. Comment écrire des tests unitaires significatifs avec Jest qui détectent de vrais bugs sans empêcher la refactorisation.

AB
Alex B.
Tests unitaires : le filet de sécurité, alias comment dormir la nuit

Pourquoi Maison Code en parle

Chez Maison Code Paris, nous agissons comme la conscience architecturale de nos clients. Nous héritons souvent de stacks “modernes” construites sans compréhension fondamentale de l’échelle.

Nous abordons ce sujet car il représente un point de pivot critique dans la maturité de l’ingénierie. Une mise en œuvre correcte différencie un MVP fragile d’une plateforme résiliente de niveau entreprise.

La crise de confiance

Vous devez refactoriser une fonction principale. Il s’agit peut-être de la fonction calculateTax() lors du paiement. C’est compliqué. Il contient des instructions « if » imbriquées. Il utilise une ancienne syntaxe. Vous voulez le nettoyer. Mais tu es terrifié. Vous vous demandez : « Si je touche à cela et que j’enfreins une règle fiscale spécifique pour les clients grossistes allemands, le saurai-je ? Si la réponse est « Non », vous êtes paralysé. Alors tu le laisses. Le « Legacy Code » demeure. Ça pourrit. Cela devient le « dossier effrayant » auquel personne ne touche. Les Tests unitaires sont le remède à cette paralysie. Ils figent le comportement du code dans le temps. Ils garantissent : “Cette fonction, étant donné l’entrée 2, DOIT renvoyer 4.” Si ce contrat est valable, vous pouvez réécrire l’implémentation interne comme vous le souhaitez. Les tests facilitent le Refactoring agressif. Sans tests, le refactoring ne fait que « deviner ».

Pourquoi Maison Code discute des tests unitaires

Chez Maison Code, nous effectuons souvent des « Missions de Sauvetage » sur des plateformes existantes. Nous trouvons des bases de code où les développeurs ont peur de déployer. “Ne touchez pas au module UserSync, il se casse si vous le regardez mal.” Cette peur paralyse l’entreprise. Les nouvelles fonctionnalités prennent des mois car 80 % du temps est consacré aux tests de régression manuels. Nous pensons que Le code testable est un code propre. Nous mettons en œuvre des suites de tests unitaires robustes en utilisant Jest/Vitest pour redonner confiance à l’équipe. Lorsque les tests sont au vert, vous déployez. Aussi simple que ça. Nous transformons le “Déploiement vendredi” d’un cauchemar en un non-événement.

L’outil : Jest / Vitest

  • Jest : Le titulaire. Maintenu par Meta. Standard dans Create React App / Next.js. Puissant mais lourd.
  • Vitest : Le challenger. Propulsé par Vite. Il est fonctionnellement identique à Jest (API compatible) mais 10x plus rapide car il utilise nativement les modules ES. Nous utiliserons ici la syntaxe Jest, car elle s’applique aux deux. Les concepts sont universels.

La stratégie : Que tester ?

C’est là que les équipes échouent. Le piège « Couverture à 100 % ». Les managers recherchent une « couverture du code à 100 % ». Ils l’ont mis dans les OKR. Cela conduit à des tests inutiles.

Mauvais test (test du framework) :

const Button = ({ label }) => <button>{label}</button>;

test('Le bouton rend', () => {
  render(<Button label="Go" />);
  expect(screen.getByText('Go')).toBeInTheDocument();
});

Ce test a une valeur faible. Il teste React. Nous savons que React fonctionne. Il a Haute maintenance. Si nous renommons « label » en « texte », le test s’interrompt, même si l’application fonctionne. Il s’agit d’un « faux négatif ».

Bonne stratégie de test : Concentrez-vous sur Business Logic et Edge Cases.

1. Fonctions pures (ROI le plus élevé)

Une fonction pure dépend uniquement des arguments et renvoie une valeur. Aucun effet secondaire. Aucun appel API. C’est un plaisir de tester. Ils fonctionnent en millisecondes.

// logique.ts
/**
 * Calcule la remise en fonction du niveau client.
 * Règles :
 * - VIP bénéficie de 20 % de réduction.
 * - Un prix négatif génère une erreur.
 * - L'employé bénéficie d'une réduction de 50 %.
 */
fonction d'exportation calculateDiscount (prix : nombre, type : 'VIP' | 'Régulier' | 'Employé'): numéro {
  if (price < 0) throw new Error('Un prix négatif est impossible');
  si (type === 'Employé') prix de retour * 0,5 ;
  si (tapez === 'VIP') prix de retour * 0,8 ; 
  prix de retour ;
}

// logique.test.ts
décrire('calculateDiscount', () => {
  test('gives 20% off to VIP', () => {
    // Organiser
    prix constant = 100 ;
    type const = 'VIP';
    // Agir
    résultat const = calculateDiscount (prix, type);
    // Affirmer
    attendre(résultat).toBe(80);
  });

  test('offre 50 % de réduction à l'employé', () => {
    expect(calculateDiscount(100, 'Employee')).toBe(50);
  });

  test('donne 0 % de réduction au Regular', () => {
    expect(calculateDiscount(100, 'Regular')).toBe(100); // 100 * 1 = 100
  });

  test('lance sur prix négatif', () => {
    // Remarque : Nous encapsulons le code appelant dans une fonction afin que Jest puisse détecter l'erreur
    expect(() => calculateDiscount(-10, 'Regular')).toThrow('Negative price');
  });
});

C’est robuste. Il documente mieux les règles métier que les commentaires.

2. La moquerie (le mal nécessaire)

La plupart du code interagit avec le monde (Base de données, API, LocalStorage). Vous ne pouvez pas exécuter un véritable appel API dans un test unitaire. C’est lent, instable et nécessite des informations d’identification. Vous vous moquez de la dépendance.

// profilutilisateur.ts
importer { fetchUserFromStripe } depuis './api' ;

fonction d'exportation asynchrone getUserStatus (id : string) {
  const user = attendre fetchUserFromStripe(id); // Dépendance externe
  if (user.delinquent) renvoie « Bloqué » ;
  renvoyer « Actif » ;
}

// profilutilisateur.test.ts
importer { fetchUserFromStripe } depuis './api' ;
plaisanterie.mock('./api'); // Se moque automatiquement du module. Remplace les fonctions réelles par jest.fn()

test('renvoie Bloqué si l'utilisateur est délinquant', async() => {
  // Configure la réponse fictive
  (fetchUserFromStripe comme jest.Mock).mockResolvedValue({ id : '1', délinquant : true });

  const status = attendre getUserStatus('1');
  
  attendre(statut).toBe('Bloqué');
  // Vérifiez que l'API a été appelée correctement pour garantir que nous transmettons l'ID
  expect(fetchUserFromStripe).toHaveBeenCalledWith('1');
});

Avertissement concernant les moqueries : les moqueries mentent. Si le vrai fetchUserFromStripe change son format de retour (par exemple, delinquent devient isDelinquent), votre test réussira toujours (parce que vous vous êtes moqué de l’ancien format), mais votre application plantera. Utilisez TypeScript pour synchroniser les simulations avec les types réels.

Test d’instantanés : fonctionnalité ou bug ?

Jest a introduit les “instantanés”. expect(component).toMatchSnapshot(). Il sérialise le composant rendu dans un fichier texte (__snapshots__/comp.test.js.snap). Si vous remplacez H1 par H2, le test échoue. Le problème : les développeurs traitent cela comme une nuisance. Ils font le test. Cela échoue. Ils tapent « jest -u » (Mise à jour) sans regarder. Les tests instantanés sont Casants. Meilleure pratique : utilisez des instantanés pour les composants très petits et stables (icônes, jetons de conception) où toute modification est suspecte. Ne les utilisez pas pour des pages complexes.

Test du code asynchrone (le piège Async/Await)

Tester les promesses est délicat. Erreur courante : oublier « wait » ou « params ».

// MAUVAIS : le test se termine avant la résolution de la promesse
test('mauvais test asynchrone', () => {
  fetchData().then(data => {
    expect(data).toBe('beurre de cacahuète');
  });
});

Si fetchData échoue, le test peut quand même réussir (faux positif) ou expirer.

Bien :

test('bon test asynchrone', async() => {
  const data = attendre fetchData();
  expect(data).toBe('beurre de cacahuète');
});

Pour les minuteries (par exemple setTimeout), utilisez Fake Timers : jest.useFakeTimers(). jest.advanceTimersByTime(1000). Cela vous permet de tester un retard de 10 secondes en 1 milliseconde.

Le débat TDD (Test Driven Development)

“Écrivez d’abord le test.” Avantages : vous oblige à concevoir l’API avant la mise en œuvre. Donne un code très propre et découplé. Inconvénients : vitesse initiale plus lente. Difficile quand on “explore” (prototypage) et que l’on ne connaît pas la structure finale. Verdict : utilisez TDD pour une logique algorithmique complexe (par exemple, analyse d’un CSV, calcul de taxe, manipulation de chaînes). Ne l’utilisez pas pour des composants d’interface utilisateur simples où vous itérez sur des pixels.

Intégration vs unité : la forme du trophée

Kent C. Dodds a officialisé le « Testing Trophy ».

  1. Analyse statique (ESLint, TypeScript) : détecte les fautes de frappe. (Le plus rapide/moins cher).
  2. Tests unitaires : testez les fonctions pures. (Rapide).
  3. Tests d’intégration : testez la connexion entre les composants. (Le point doux).
  4. Tests E2E : testez le navigateur complet. (Lent/cher).

Recommandation : Écrivez principalement des Tests d’intégration. Testez LoginForm + SubmitButton + ApiMock. Ne testez pas SubmitButton de manière isolée. Testez que “Cliquer sur Soumettre appelle l’API de connexion”.

##FAQ

Q : Comment tester les fonctions privées ? R : Ne le faites pas. Testez l’API publique. La fonction privée est un détail d’implémentation. Si vous testez des fonctions privées, vous vous enfermez dans cette implémentation. Vous perdez la capacité de refactoriser.

Q : Intégration CI/CD ? R : Les tests doivent être exécutés à chaque Pull Request. Si le « test npm » échoue, le bouton de fusion doit être désactivé. Cela maintient la politique du « Green Master ».

Conclusion

Les tests sont un investissement. Vous payez d’avance (Time). Vous obtenez des dividendes pour toujours (stabilité, rapidité de refactorisation, documentation). Une base de code sans tests est une « base de code héritée » dès le premier jour. Une base de code avec des tests est un “Atout”. Il prend de la valeur.

Implémentation paralysée ?

Si votre équipe est paralysée par la peur de casser le code existant, Maison Code est la solution. Nous effectuons des « Raids de refactorisation » au cours desquels nous auditons votre base de code, ajoutons une couverture de test et nettoyons la dette technologique qui vous ralentit. Nous formons votre équipe à rédiger des tests qui ne sont pas nuls.


Engagez nos Architectes.