Cours NestJS · Module 7: Tests

Test Automation - les contrôles de qualité automatiques

6 min de lecture
Dans cette leçon6

Tu disposes d'un jeu complet de tests : unitaires, d'intégration, E2E, avec doublures et rapport de couverture. Mais tu les lances à la main. Il suffit d'une soirée pressée pour que quelqu'un pousse une modification sans vérification - et tout cet arsenal se révèle inutile.

La légion ne se fie pas à la mémoire des sentinelles. Elle poste des gardes permanentes : l'inspection se fait toute seule, à un moment fixé, quel que soit celui qui est de service. C'est cela, l'automatisation des tests - transférer la responsabilité d'un humain à une machine.

La garde permanente - des tests à chaque modification

La garde la plus simple lance les tests à chaque envoi de code :

1# .github/workflows/ci.yml
2name: CI
3on: [push, pull_request]
4
5jobs:
6  test:
7    runs-on: ubuntu-latest
8    steps:
9      - uses: actions/checkout@v4
10      - run: npm ci
11      - run: npm run test
12      - run: npm run test:e2e

Cela se lit comme un ordre du jour. on: [push, pull_request] fixe le moment de l'inspection. npm ci installe les dépendances strictement d'après package-lock.json - contrairement à npm install, il ne met rien à jour, la garde vérifie donc exactement les versions que tu as enregistrées. Puis les tests s'exécutent.

L'essentiel est invisible dans ce fichier : chaque commande doit renvoyer zéro. Si npm run test se termine en échec, la garde donne l'alerte et la modification n'entre pas dans la branche principale. Toute la puissance de l'automatisation tient à ce seul code de sortie.

Une garde verte qui ment

Nous arrivons ici au point le plus important de cette leçon. Un test automatisé vaut exactement ce que vaut son honnêteté - et un test asynchrone peut passer sans rien vérifier.

Regarde ces deux versions :

1// MAL - le test se termine avant que la promesse ne se règle
2it('trouve un légionnaire', () => {
3  service.findById(1);
4});
5
6// BIEN - le test attend le résultat
7it('trouve un légionnaire', async () => {
8  const legionary = await service.findById(1);
9  expect(legionary.name).toBe('Marcus');
10});

Dans la première version, la fonction de test se termine aussitôt après l'appel de la méthode. Jest compte le test comme réussi, car rien n'a levé d'exception à cet instant - alors que la méthode ne fait que commencer son travail. Si elle renvoyait de mauvaises données ou échouait une seconde plus tard, personne ne l'apprendrait.

La règle est brève et sans exception : un test touchant du code asynchrone doit être async, et chaque appel renvoyant une promesse doit être précédé d'await. Un await oublié est la cause la plus fréquente des pipelines verts au-dessus d'un code cassé.

Vérifier les exceptions - le cas synchrone

Nous testons les échecs autant que les réussites. Quand une méthode doit lever une exception, il nous faut une assertion qui le confirme :

1it('refuse une solde négative', () => {
2  expect(() => service.validatePay(-100)).toThrow();
3});

Note la forme de l'argument : nous passons à expect une fonction, et non le résultat de son appel. Si nous avions écrit expect(service.validatePay(-100)), l'exception partirait immédiatement, hors du contrôle de Jest, et le test échouerait au lieu de passer. L'enveloppe () => ... permet à Jest d'appeler la méthode lui-même et d'attraper ce qui en tombe.

toThrow() sans argument accepte n'importe quelle exception. Tu peux restreindre : toThrow(BadRequestException) vérifie le type, et toThrow('La solde ne peut être négative') le message.

Vérifier les exceptions - le cas asynchrone

Une méthode asynchrone ne lève pas d'exception - elle renvoie une promesse rejetée. Le toThrow() de la section précédente ne fonctionnera donc pas ici ; il faut le modificateur rejects :

1it('lève NotFoundException pour un id inconnu', async () => {
2  jest.spyOn(repo, 'findOne').mockResolvedValue(null);
3
4  await expect(service.findById(999)).rejects.toThrow(NotFoundException);
5});

Parcourons cette assertion, car elle se compose de quatre éléments dans un ordre fixé. await expect( ouvre l'attente - et cet await initial est ce qu'on oublie le plus souvent ; sans lui, le test se termine avant que la promesse ne se règle et passe de nouveau à tort. Vient ensuite service.findById(999) - ici, nous n'enveloppons pas l'appel dans une fonction, car une promesse est une valeur ordinaire que l'on peut transmettre. Puis .rejects bascule l'assertion vers le rejet, et .toThrow(NotFoundException) vérifie le type.

L'ordre de travail pour un tel test est toujours le même : configure la doublure pour provoquer l'erreur, appelle la méthode, utilise rejects.toThrow(), et précise enfin le type et le message. Ici, la doublure du repository renvoie null, ce qui force le service à lever NotFoundException - nous testons donc la gestion de l'erreur, et non l'erreur de base elle-même.

La barrière locale - avant que le code ne quitte le camp

La garde en CI attrape tout, mais répond après quelques minutes. Nous posons un contrôle plus rapide sur ta propre machine :

1# .husky/pre-commit
2npm run lint
3npm run test -- --onlyChanged

Husky raccroche ce script à git commit - si une commande renvoie une erreur, le commit n'a pas lieu. Le drapeau --onlyChanged demande à Jest de n'exécuter que les tests liés aux fichiers modifiés, la barrière dure donc des secondes et non des minutes.

Note le partage des rôles : le hook local est rapide et sélectif, la garde en CI lente et complète. Le hook est là pour attraper les faux pas évidents avant qu'ils ne coûtent du temps à quelqu'un ; il ne remplace pas l'exécution complète. Et c'est ce que je te recommande : ne mets pas tous les tests dans le hook, car la première chose que ton équipe apprendra sera --no-verify.

Récapitulatif

Les gardes sont postées, l'inspection se fait toute seule :

  • l'automatisation transfère l'exécution des tests d'un humain à une machine - c'est le code de sortie qui décide,
  • on: [push, pull_request] fixe le moment de l'inspection, npm ci installe exactement les versions de package-lock.json,
  • un test touchant du code asynchrone doit être async et utiliser await - sans cela il passe sans rien vérifier,
  • exception synchrone : expect(() => methode()).toThrow() - expect reçoit une fonction, non son résultat,
  • toThrow() accepte un type d'exception ou un fragment de message pour restreindre la vérification,
  • promesse rejetée : await expect(methode()).rejects.toThrow(NotFoundException) - ici nous passons la promesse elle-même, sans enveloppe,
  • l'ordre : configure la doublure pour échouer, appelle la méthode, rejects.toThrow(), vérifie le type et le message,
  • un hook pre-commit avec --onlyChanged offre une barrière locale rapide ; laisse l'exécution complète à la CI.

Dans la prochaine leçon, tu affronteras un projet - tester de bout en bout un système de gestion de flotte. En attendant, retiens ceci : une garde permanente vaut ce que vaut l'honnêteté des tests qu'elle lance - et un test sans await signale que tout est calme, alors que personne n'a vérifié les portes.

Code de cette leçon : src/test-automation.ts
1// Test Automation - Contrôles qualité automatiques
2
3// 1. Configuration de Jest dans package.json
4const jestConfig = {
5  moduleeFileExtensions: ['js', 'json', 'ts'],
6  rootDir: 'src',
7  testRegex: '.*\\.spec\\.ts$',
8  transform: { '^.+\\.(t|j)s$': 'ts-jest' },
9  collectCoverageFrom: ['**/*.(t|j)s'],
10  coverageDirectory: '../coverage',
11  testEnvironment: 'node',
12  coverageThreshold: {
13    global: {
14      branches: 80,
15      functions: 80,
16      lines: 80,
17      statements: 80,
18    },
19  },
20};
21
22// 2. Pre-commit hooks avec Husky
23const huskyConfig = {
24  hooks: {
25    'pre-commit': 'npm run lint && npm run test',
26    'pre-push': 'npm run test:cov && npm run test:e2e',
27  },
28};
29
30// 3. Workflow GitHub Actions
31const ciYaml = `
32name: Roman Empire CI
33on: [push, pull_request]
34jobs:
35  test:
36    runs-on: ubuntu-latest
37    strategy:
38      matrix:
39        node-version: [18.x, 20.x]
40    steps:
41      - uses: actions/checkout@v4
42      - uses: actions/setup-node@v4
43        with:
44          node-version: \${{ matrix.node-version }}
45      - run: npm ci
46      - run: npm run lint
47      - run: npm run test:cov
48      - run: npm run test:e2e
49`;
50
51// 4. Test Reporter
52class TestReporter {
53  private results: Array<{
54    suite: string;
55    test: string;
56    passed: boolean;
57    duration: number;
58  }> = [];
59
60  addResult(
61    suite: string, test: string,
62    passed: boolean, duration: number
63  ) {
64    this.results.push({ suite, test, passed, duration });
65  }
66
67  generateReport() {
68    const total = this.results.length;
69    const passed = this.results.filter(r => r.passed).length;
70    const failed = total - passed;
71    const avgDuration = this.results.reduce(
72      (sum, r) => sum + r.duration, 0
73    ) / total;
74
75    return {
76      summary: {
77        total,
78        passed,
79        failed,
80        passRate: `${((passed / total) * 100).toFixed(1)}%`,
81        avgDuration: `${avgDuration.toFixed(2)}ms`,
82      },
83      failures: this.results
84        .filter(r => !r.passed)
85        .map(r => `${r.suite} > ${r.test}`),
86    };
87  }
88}
89
90// 5. Exemple d'utilisation
91const reporter = new TestReporter();
92reporter.addResult('LegionService', 'findAll', true, 5.2);
93reporter.addResult('LegionService', 'create', true, 8.1);
94reporter.addResult('ProvinceCtrl', 'GET /provinces', true, 12.3);
95reporter.addResult('AuthGuard', 'block unauthorized', true, 3.7);
96
97console.log(JSON.stringify(reporter.generateReport(), null, 2));
98

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Pour tester une méthode asynchrone en Jest, le test doit :

  2. 2. Pour vérifier si une méthode lève une exception, on utilise :

Tâches pratiques dans le jeu

  • Éditeur de code

    Teste le scénario où le service lève une NotFoundException

  • Tri vertical

    Ordonne les étapes pour tester une exception asynchrone :

  • Clique dans l'ordre

    Ordonne l'assertion testant une Promise rejetée :