Skip to content
Learni
Voir tous les tutoriels
DevOps

Comment implémenter un pipeline CI/CD de tests avancés en 2026

Read in English

Introduction

En 2026, les pipelines CI/CD ne se contentent plus d'un simple npm test. Un vrai système de tests avancés intègre unitaires, d'intégration (avec mocks ou conteneurs), E2E (via Playwright), une matrix multi-versions Node.js, du caching intelligent (pnpm store), des thresholds de coverage stricts (>90%) et des artifacts pour rapports HTML. Cela réduit les merges défectueux de 70% selon les études GitHub.

Ce tutoriel avancé vous guide pas à pas pour un projet Node.js réel : une API mathématique simple, testée exhaustivement. Imaginez comme un convoyeur automobile : chaque étape valide une pièce avant assemblage. À la fin, votre repo GitHub déploiera uniquement du code blindé, prêt prod. Idéal pour équipes seniors gérant microservices critiques. (142 mots)

Prérequis

  • Node.js 20+ et pnpm 9+
  • Compte GitHub avec repo vide
  • Connaissances avancées : Jest, Supertest, Playwright, Docker basics
  • Git installé localement
  • VS Code avec extensions YAML et TypeScript

Initialiser le projet avec pnpm

terminal
mkdir ci-cd-testing-advanced
cd ci-cd-testing-advanced
pnpm init
pnpm add -D typescript @types/node jest @types/jest ts-jest supertest @types/supertest playwright @playwright/test pnpm@latest
pnpm add express
pnpm tsx --version

Ce script initialise un projet Node.js avec pnpm pour un caching optimal en CI. On installe TypeScript, Jest pour unitaires, Supertest pour intégration, Playwright pour E2E. Piège : utiliser pnpm au lieu de npm évite les doublons et accélère les installs CI de 50%.

Configurer package.json et tsconfig

package.json
{
  "name": "ci-cd-testing-advanced",
  "version": "1.0.0",
  "scripts": {
    "build": "tsc",
    "test:unit": "jest --coverage --passWithNoTests",
    "test:integration": "jest --config jest.integration.config.js",
    "test:e2e": "playwright test",
    "test:ci": "pnpm run test:unit && pnpm run test:integration && pnpm run test:e2e",
    "dev": "tsx watch src/index.ts"
  },
  "jest": {
    "preset": "ts-jest",
    "testEnvironment": "node",
    "collectCoverageFrom": ["src/**/*.ts", "!src/**/*.d.ts"],
    "coverageThreshold": {
      "global": {
        "branches": 90,
        "functions": 90,
        "lines": 90,
        "statements": 90
      }
    }
  }
}

Package.json définit tous les scripts CI-ready avec coverage thresholds à 90% : échec si <90%. test:ci chain tout. Piège : collectCoverageFrom ignore les .d.ts pour metrics précises ; sans ça, coverage gonflé fausse les rapports.

Implémenter l'API et tests unitaires

src/math.ts
export class MathService {
  static add(a: number, b: number): number {
    if (typeof a !== 'number' || typeof b !== 'number') throw new Error('Inputs must be numbers');
    return a + b;
  }

  static divide(a: number, b: number): number {
    if (b === 0) throw new Error('Division by zero');
    return a / b;
  }

  static fibonacci(n: number): number {
    if (n < 0) throw new Error('Negative input');
    if (n <= 1) return n;
    let a = 0, b = 1;
    for (let i = 2; i <= n; i++) {
      const next = a + b;
      a = b;
      b = next;
    }
    return b;
  }
}

export interface MathRequest { op: 'add' | 'divide' | 'fib'; a: number; b?: number; }

MathService : cœur métier testable. Unitaires suivants valideront branches edge cases. Analogie : comme un circuit électrique, chaque chemin (if/loop) doit être 'allumé' par un test. Piège : sans validation types, tests int grisent les unitaires.

Tests unitaires Jest complets

tests/unit/math.test.ts
import { MathService, MathRequest } from '../../src/math';

describe('MathService', () => {
  describe('add', () => {
    it('should add two numbers', () => expect(MathService.add(2, 3)).toBe(5));
    it('should throw on non-numbers', () => {
      expect(() => MathService.add('2' as any, 3)).toThrow('Inputs must be numbers');
    });
  });

  describe('divide', () => {
    it('should divide', () => expect(MathService.divide(10, 2)).toBe(5));
    it('should throw on zero divisor', () => expect(() => MathService.divide(10, 0)).toThrow('Division by zero'));
  });

  describe('fibonacci', () => {
    it.each([0, 1, 5, 10])( 'fib(%i) = %i', (n: number, expected: number) => {
      expect(MathService.fibonacci(n)).toBe(expected);
    });
    it('throws on negative', () => expect(() => MathService.fibonacci(-1)).toThrow('Negative input'));
  });
});

Tests 100% coverage : table-driven pour fibo (efficace), edge cases prioritaires. --passWithNoTests en CI évite faux positifs. Piège : ignorer it.each ralentit ; ici, matrix-like pour 4 cas en 1 test.

Workflow GitHub Actions : Unit + Matrix

.github/workflows/unit-tests.yml
name: Unit Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]
    steps:
    - uses: actions/checkout@v4
    - uses: pnpm/action-setup@v4
      with:
        version: latest
    - uses: actions/setup-node@v4
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'pnpm'
    - run: pnpm install --frozen-lockfile
    - run: pnpm run test:unit
      env:
        CI: true
    - uses: actions/upload-artifact@v4
      if: always()
      with:
        name: coverage-${{ matrix.node-version }}
        path: coverage/

Premier workflow : matrix Node 18-22 teste compat multi-versions en parallèle (x3 vitesse). Caching pnpm + --frozen-lockfile = installs <10s. Artifacts préservent rapports même sur échecs. Piège : sans if: always(), artifacts perdus sur fail.

API Express + Tests d'intégration

src/index.ts
import express from 'express';
import { MathService, MathRequest } from './math';

const app = express();
app.use(express.json());

app.post('/math', (req, res) => {
  try {
    const { op, a, b }: MathRequest = req.body;
    let result;
    switch (op) {
      case 'add': result = MathService.add(a, b!); break;
      case 'divide': result = MathService.divide(a, b!); break;
      case 'fib': result = MathService.fibonacci(a); break;
      default: return res.status(400).json({ error: 'Invalid op' });
    }
    res.json({ result });
  } catch (e) {
    res.status(400).json({ error: (e as Error).message });
  }
});

const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`Server on ${port}`));

API REST complète sur MathService. POST /math gère switch + errors. Tests int suivants simulent HTTP réel. Piège : sans express.json(), body parsing échoue ; toujours middleware first.

Tests d'intégration Supertest

tests/integration/api.test.ts
import request from 'supertest';
import app from '../../src/index';

describe('API Integration', () => {
  it('POST /math add success', async () => {
    const res = await request(app).post('/math').send({ op: 'add', a: 2, b: 3 });
    expect(res.status).toBe(200);
    expect(res.body).toEqual({ result: 5 });
  });

  it('POST /math divide error', async () => {
    const res = await request(app).post('/math').send({ op: 'divide', a: 10, b: 0 });
    expect(res.status).toBe(400);
    expect(res.body.error).toBe('Division by zero');
  });

  it('POST /math invalid op', async () => {
    const res = await request(app).post('/math').send({ op: 'foo' as any, a: 1 });
    expect(res.status).toBe(400);
    expect(res.body.error).toBe('Invalid op');
  });
});

Supertest simule req HTTP sans serveur lancé (rapide). Couvre happy/edge paths. Ajoutez jest.integration.config.js pour isoler : { testMatch: '/integration//*.test.ts' }. Piège : oublier async/await crash les tests.

Workflow Intégration + E2E avec Docker

.github/workflows/integration-e2e.yml
name: Integration & E2E
on: [push, pull_request]

jobs:
  integration:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: pnpm/action-setup@v4
    - uses: actions/setup-node@v4
      with: { node-version: 20, cache: 'pnpm' }
    - run: pnpm install --frozen-lockfile
    - run: pnpm run test:integration

  e2e:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: pnpm/action-setup@v4
    - uses: actions/setup-node@v4
      with: { node-version: 20, cache: 'pnpm' }
    - run: pnpm install --frozen-lockfile
    - uses: microsoft/playwright-github-action@v1
    - run: pnpm exec playwright install --with-deps
    - run: pnpm run build
    - run: pnpm run test:e2e
      env: { PORT: 3000 }
    - uses: actions/upload-artifact@v4
      if: always()
      with:
        name: playwright-report
        path: playwright-report/

Jobs parallèles : int sans Docker (rapide), E2E avec Playwright official action (browsers auto). Needs playwright.config.ts basique. Piège : sans --with-deps, browsers crash en CI headless.

Config Playwright E2E minimal

playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests/e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
  ],
});

Config E2E : parallel safe, retries CI-only, HTML report artifact-ready. Lancez serveur dev pour local. Piège : forbidOnly bloque test.only() en CI, évitant skips accidentels.

Bonnes pratiques

  • Matrix + Caching : Toujours tester 3 Node versions ; pnpm cache divise temps x4.
  • Thresholds stricts : 90% min, fail-fast en PR.
  • Artifacts always() : Rapports visibles même sur fails.
  • Needs/depends_on : Chain jobs (unit → int → e2e) pour optim.
  • Secrets env : DB creds en GitHub Secrets pour tests réels.

Erreurs courantes à éviter

  • Oublier frozen-lockfile : installs drift, builds instables.
  • Pas de coverage thresholds : merges low-quality code.
  • E2E sans baseURL : timeouts localhost.
  • Matrix sans strategy : jobs séquentiels, CI lente >10min.

Pour aller plus loin

Intégrez SonarQube pour code smells ou ArgoCD pour CD Kubernetes. Découvrez nos formations DevOps avancées Learni : CI/CD GitOps, observabilité full-stack.