Aller au contenu principal

Backend pratique · L2 · Section 5/6

Tests et observabilité

Progression

Points d’expérience : XPSérie de jours consécutifs : · —Progression du module : — / —compris

#Tests et observabilité

Des tests unitaires pour la logique pure, d’intégration pour les endpoints critiques et de contrat pour stabiliser l’API côté client. Ajoutez des logs structurés (JSON), des métriques (latences, taux d’erreur) et de la traçabilité (correlation id) pour diagnostiquer. Les tests ne remplacent pas l’observabilité ; l’observabilité ne remplace pas les tests.

Avant de continuer, vous devez savoir écrire un test avec un framework (Vitest, Jest) et avoir suivi le fil rouge POST /signup depuis le premier chapitre. À la fin de ce chapitre, vous saurez verrouiller ce parcours par des tests d’intégration et de contrat, et savoir, en production, ce qui se passe par logs, métriques et traces.

Les tests d’intégration démarrent l’app avec des dépendances éphémères (DB en mémoire/conteneur) et valident des parcours réalistes. Les mocks s’utilisent avec mesure : moquez le réseau lointain, pas la base si votre code dépend des index et transactions. Les tests de charge légers capturent régressions flagrantes (latences x10, fuites mémoire) avant la prod.

Mini‑exercice : testez POST /signup (chemin heureux, email déjà pris, mot de passe trop court) puis ajoutez un test de contrat qui vérifie la structure de l’erreur (Problem Details) et la présence d’un traceId.

#Animation : pyramide de tests pragmatique

Unitaires
Rapides, isolés, logique pure
Intégration
Infra éphémère; endpoints critiques
Contrats
Stabiliser l’API (client/serveur)
E2E
Parcours clés; flakiness maîtrisée
Perf
Smoke load; budgets en CI

#Diagramme : traçage d’une requête (logs, métriques, traces)

Client
API
DB
Observabilité
1. POST /signup
2. INSERT users (tx)
3. OK
4. 201 Created
5. Log JSON + métriques (latence, status) + traceId

#Anti‑flakiness checklist

  • Données et seeds déterministes ; horloges figées (fake timers) pour éviter les effets du temps.
  • Retrys uniquement sur opérations idempotentes ; backoff borné ; limites claires.
  • Infra éphémère pour l’intégration (DB conteneur/mémoire) ; pas d’appels réseau externes réels.
  • Timeouts explicites par test ; journaux/artefacts collectés automatiquement en cas d’échec.
  • Tests de contrat pour stabiliser l’interface et éviter les cassures silencieuses côté client.

#Exemples de code pratiques

#Intégration : POST /signup avec Supertest

tsts

1import request from 'supertest'2import { app } from '../src/app'3import { createTestDb, resetDb, destroyDb } from './helpers/db'4 5beforeAll(async () => { await createTestDb() })6afterAll(async () => { await destroyDb() })7beforeEach(async () => { await resetDb() })8 9describe('POST /signup', () => {10  it('chemin heureux', async () => {11    const res = await request(app)12      .post('/signup')13      .send({ email: 'a@b.c', password: 'S3cure#123' })14      .set('x-correlation-id', 't-1')

Notez les trois tests : nominal (201), conflit métier (409), refus de validation (422). Ce triplet est la couverture minimale de tout endpoint d’écriture : succès, échec métier, échec de contrat. Si vous devez en écrire un seul, écrivez le conflit : c’est lui qui attrape les exceptions non gérées qui fuient en 500.

#Écrire le test d’abord (rouge, puis vert)

Le test de contrat ci-dessous a été écrit avant le correctif, et il échouait. C’est la preuve qu’il teste quelque chose :

tsts

1// Étape 1: ROUGE. Ce test échoue tant que l'erreur n'est pas un Problem Details2test('Problem Details shape (rouge avant correctif)', async () => {3  const res = await request(app).post('/signup').send({ email: 'a@b.c', password: '123' })4  expect(res.status).toBe(422)5  expect(res.body.type).toBeDefined()     // échoue si le corps est { error: '...' }6  expect(res.body.traceId).toBeDefined()  // échoue si la corrélation n'est pas propagée7})
tsts

1// Étape 2: VERT. Le handler produit désormais le format attendu2app.post('/signup', validate(SignupSchema), async (req, res) => {3  // ... création ...4})

Un test qui n’a jamais échoué ne prouve rien : il peut passer pour la mauvaise raison (assertion trop faible, mock qui retourne toujours). Cassez volontairement le code une fois, vérifiez que le test rougit, remettez le code.

#Tests de contrat : Problem Details + traceId

tsts

1import request from 'supertest'2import { z } from 'zod'3import { app } from '../src/app'4 5const Problem = z.object({6  type: z.string().url(),7  title: z.string(),8  status: z.number().int(),9  detail: z.string().optional(),10  traceId: z.string().optional(),11  errors: z.array(z.object({ field: z.string(), message: z.string() })).optional(),12})13 14test('Problem Details shape', async () => {

Le test de contrat diffère du test d’intégration par sa cible : l’intégration vérifie un comportement (l’email dupliqué est rejeté), le contrat vérifie une forme (toute erreur 4xx ressemble à ceci). La forme est ce que les clients consomment ; la casser sans test rouge, c’est casser des applications que vous ne contrôlez pas.

#Logs structurés + correlation id

tsts

1import pino from 'pino'2import { randomUUID } from 'node:crypto'3export const logger = pino({ level: process.env.LOG_LEVEL || 'info' })4export function withCorrelationId(req, _res, next) {5  req.id = req.get('x-correlation-id') || randomUUID()6  req.logger = logger.child({ traceId: req.id })7  next()8}9 10app.use(withCorrelationId)11 12app.post('/signup', async (req, res) => {13  const start = Date.now()14  try {

#Métriques : les quatre nombres qui comptent

Les logs racontent une requête ; les métriques racontent la population. Quatre séries suffisent à diagnostiquer la plupart des incidents d’une API :

tsts

1import client from 'prom-client'2const httpDuration = new client.Histogram({3  name: 'http_request_duration_ms',4  help: 'Latence HTTP en ms',5  labelNames: ['route', 'method', 'status'],6  buckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500],7})8const httpTotal = new client.Counter({9  name: 'http_requests_total',10  help: 'Requêtes HTTP totales',11  labelNames: ['route', 'method', 'status'],12})13 14app.use((req, res, next) => {

Ce qu’elles répondent, en production :

  • Taux d’erreur (5xx / total) : la santé globale. Une alerte à 1 % sur cinq minutes détecte la plupart des régressions.
  • Latence p50/p95/p99 par route : le p50 dit « l’expérience médiane », le p99 dit « la promesse tenue ». Une p99 qui grimpe sans p50 signale des requêtes lentes (requête sans index, lock).
  • Débit (req/s) : distingue « le service est lent » de « le service est saturé ».
  • Saturation (connexions DB, file d’attente, mémoire) : la cause racine quand tout le reste dérape.

Une métrique sans alerte est un graphique décoratif : chaque série que vous ajoutez doit avoir un seuil et un destinataire.

Propagation W3C traceparent
export function withTracePropagation(req, res, next) {
const tp = req.get('traceparent')
if (tp) req.traceparent = tp
res.setHeader('traceparent', req.traceparent || `00-${crypto.randomUUID().replace(/-/g,'').slice(0,32)}-0000000000000000-01`)
next()
}

#Smoke de charge (10s)

bashbash

1npx autocannon -m POST -H 'content-type: application/json' -d 10 -c 20 -b '{"email":"a@b.c","password":"S3cure#123"}' http://localhost:3000/signup

Un smoke de charge ne prouve pas la tenue en production ; il attrape les régressions grossières : latence multipliée par dix après un changement de requête, fuite mémoire visible sur la durée, épuisement du pool de connexions. Comparez toujours à une mesure de référence, pas à un chiffre absolu.

Correction guidée de l’exercice
  1. Triplet d’intégration : 201 (corps contient user.id), 409 (deuxième inscription avec le même email, type documenté), 422 (mot de passe court, errors[] mentionne password). Base éphémère recréée par suite, resetDb() entre chaque test.
  2. Test de contrat : schéma Zod sur le corps d’erreur (type/title/status/detail/traceId), exécuté sur plusieurs statuts (422, 409, 500 simulé). Le même schéma doit passer partout : c’est la définition même d’un contrat.
  3. Preuve rouge : avant d’écrire le format Problem Details dans le handler, le test de contrat échoue sur type manquant. Gardez ce témoignage (capture CI) : c’est la démonstration que le test défend un vrai comportement.

#Quiz rapide

Quelle stratégie d’environnement privilégier pour des tests d’intégration fiables ?
Quelle stratégie d’environnement privilégier pour des tests d’intégration fiables ?
Quel signal doit figurer dans chaque log pour corréler une requête ?
Quel signal doit figurer dans chaque log pour corréler une requête ?
La latence p99 d’une route grimpe, la p50 reste stable. Quel diagnostic prioritaire ?
La latence p99 d’une route grimpe, la p50 reste stable. Quel diagnostic prioritaire ?