Tests et observabilité
Progression
#Tests et observabilité
Des tests unitaires pour la logique pure, d’intégration pour les endpoints critiques et de contract 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.
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
#Diagramme: traçage d’une requête (logs, métriques, traces)
#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
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')
Démarrez une base jetable par suite de tests (containers, mémoire) et appliquez les migrations au setup. Nettoyez entre tests (TRUNCATE/transactions) pour l’isolation.
#Tests de contrat: Problem Details + traceId
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 () => {
#Logs structurés + correlation id
1import pino from 'pino'2import { randomUUID } from 'node:crypto'3export const logger = pino({ level: process.env.LOG_LEVEL || 'info' })4 5export function withCorrelationId(req, _res, next) {6 req.id = req.get('x-correlation-id') || randomUUID()7 req.logger = logger.child({ traceId: req.id })8 next()9}10 11app.use(withCorrelationId)12 13app.post('/signup', async (req, res) => {14 const start = Date.now()
#Smoke de charge (10s)
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