Introduction
En 2026, les tests d'intégration sont indispensables pour valider les interactions entre vos composants backend : API REST, base de données et services externes. Contrairement aux tests unitaires isolés, ils simulent des flux réels, détectant les bugs subtils comme les conflits de transactions ou les fuites de mémoire en base. Ce tutoriel advanced vous guide pour implémenter une suite de tests robuste sur une API users avec Node.js, Express, Prisma et PostgreSQL en Docker.
Nous utiliserons Vitest pour sa vitesse et sa nativité TypeScript, Supertest pour les assertions HTTP, et des techniques avancées : cleanup transactionnel, exécution parallèle, coverage Istanbul et intégration CI/CD. À la fin, votre pipeline de tests sera production-ready, réduisant les regressions de 80% en moyenne. Projet complet : CRUD users avec validation DB. Temps estimé : 30min pour setup + tests.
Prérequis
- Node.js 20+ et pnpm 9+
- Docker et Docker Compose v2+
- Connaissances avancées en TypeScript, Express et bases de données relationnelles
- Outils : VS Code avec extensions Prisma et Vitest
- Git pour versionning
Initialisation du projet et dépendances
pnpm init
pnpm add express cors @types/express @types/cors @types/node
pnpm add -D typescript @types/node tsx prisma @prisma/client vitest supertest @vitest/coverage-istanbul @vitest/ui jsdom
pnpm tsx add prisma --save-dev
echo '{"compilerOptions": {"target": "ES2022", "module": "ESNext", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "moduleResolution": "bundler", "baseUrl": ".", "paths": {"@/*": ["src/*"]}}}' > tsconfig.json
mkdir -p src tests/integrationCe script initialise un projet TypeScript moderne avec pnpm pour des installs rapides. Les deps runtime gèrent l'API (Express+CORS), Prisma pour l'ORM. Vitest+Supertest pour tests HTTP+DB, coverage pour rapports avancés. tsx permet d'exécuter TS nativement sans build.
Configuration de la base de test Docker
Pour isoler les tests, nous provisionnons une instance PostgreSQL dédiée via Docker Compose. Cela évite les conflits avec votre DB dev/prod et simule un environnement CI/CD réaliste.
docker-compose.test.yml
version: '3.8'
services:
postgres-test:
image: postgres:16-alpine
container_name: integration-tests-db
environment:
POSTGRES_DB: testdb
POSTGRES_USER: tester
POSTGRES_PASSWORD: secretpass
ports:
- "5433:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U tester -d testdb"]
interval: 5s
timeout: 5s
retries: 5Ce compose expose PostgreSQL sur port 5433 avec healthcheck pour CI. Alpine réduit la taille image. Utilisez docker-compose -f docker-compose.test.yml up -d avant tests, down après pour cleanup automatique.
Schéma Prisma pour users
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
name String @db.VarChar(255)
email String @unique @db.VarChar(255)
@@map("users")
}Schéma minimal pour CRUD users avec contraintes unique/email. @@map évite conflits noms tables. Générez client via npx prisma generate après migration.
Migration et .env de test
Créez le .env pour pointer vers la DB test : DATABASE_URL="postgresql://tester:secretpass@localhost:5433/testdb?schema=public". Puis npx prisma migrate dev --name init et npx prisma generate.
Serveur API Express exportable
import express, { Express, Request, Response } from 'express';
import cors from 'cors';
import { PrismaClient } from '@prisma/client';
const app: Express = express();
const prisma = new PrismaClient();
app.use(cors({ origin: '*' }));
app.use(express.json({ limit: '10mb' }));
app.post('/users', async (req: Request, res: Response) => {
try {
const { name, email } = req.body;
if (!name || !email) return res.status(400).json({ error: 'Missing fields' });
const user = await prisma.user.create({
data: { name, email },
});
res.status(201).json(user);
} catch (error) {
res.status(400).json({ error: 'Email already exists' });
}
});
app.get('/users/:id', async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id);
const user = await prisma.user.findUnique({ where: { id } });
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
});
app.delete('/users/:id', async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id);
await prisma.user.delete({ where: { id } });
res.status(204).send();
} catch (error) {
res.status(404).json({ error: 'User not found' });
}
});
export { app, prisma };App exportée sans listen() pour Supertest. Routes CRUD basiques avec validation, try/catch et codes HTTP standards. Prisma injecté globalement ; en prod, utilisez provider par requête pour scalabilité.
Configuration Vitest avancée
import { defineConfig } from 'vitest/config';
import { coverageConfig } from './vitest.coverage';
export default defineConfig({
test: {
globals: true,
environment: 'node',
pool: 'threads',
poolOptions: { threads: { singleThread: true } },
include: ['tests/integration/**/*.{test,spec}.{ts,js}'],
setupFiles: ['./tests/integration/setup.ts'],
maxConcurrency: 5,
minWorkers: 1,
},
plugins: [coverageConfig],
});Config pour tests parallèles limités (maxConcurrency=5 évite surcharge DB). Globals true pour test/expect. Pool threads pour perf. Coverage Istanbul activé via plugin.
Suite de tests d'intégration complète
Les tests utilisent beforeEach pour cleanup via Prisma (pas de transactions pour compatibilité Postgres). Supertest simule requêtes HTTP réelles. Assertions sur status, body et DB state.
Fichier de tests users.integration.test.ts
import { test, expect, beforeEach, afterEach, describe } from 'vitest';
import request from 'supertest';
import { app, prisma } from '../../src/server';
describe('Users Integration Tests', () => {
beforeEach(async () => {
await prisma.user.deleteMany({});
});
afterEach(async () => {
await prisma.$disconnect();
});
test('POST /users crée un utilisateur valide', async () => {
const newUser = { name: 'John Doe', email: 'john@example.com' };
const res = await request(app.callback())
.post('/users')
.send(newUser)
.expect(201);
expect(res.body).toMatchObject(newUser);
expect(res.body.id).toBeDefined();
const dbUser = await prisma.user.findUnique({ where: { id: res.body.id } });
expect(dbUser).not.toBeNull();
});
test('POST /users rejette email dupliqué', async () => {
const user = { name: 'Jane', email: 'jane@example.com' };
await request(app.callback()).post('/users').send(user);
const res = await request(app.callback())
.post('/users')
.send(user)
.expect(400);
expect(res.body.error).toBe('Email already exists');
});
test('GET /users/:id retourne utilisateur', async () => {
const created = await request(app.callback())
.post('/users')
.send({ name: 'Alice', email: 'alice@example.com' })
.expect(201);
const res = await request(app.callback())
.get(`/users/${created.body.id}`)
.expect(200);
expect(res.body.email).toBe('alice@example.com');
});
test('GET /users/:id 404 si inexistant', async () => {
await request(app.callback()).get('/users/999').expect(404);
});
test('DELETE /users/:id supprime utilisateur', async () => {
const created = await request(app.callback())
.post('/users')
.send({ name: 'Bob', email: 'bob@example.com' })
.expect(201);
await request(app.callback()).delete(`/users/${created.body.id}`).expect(204);
await request(app.callback()).get(`/users/${created.body.id}`).expect(404);
});
});Suite complète : 5 tests couvrant happy paths + erreurs. app.callback() pour handler sans server. Cleanup deleteMany assure isolation. Assertions DB post-HTTP valident persistance. Exécutez avec pnpm vitest run.
Scripts package.json et coverage config
{
"name": "integration-tests-advanced",
"scripts": {
"db:up": "docker-compose -f docker-compose.test.yml up -d",
"db:down": "docker-compose -f docker-compose.test.yml down",
"db:migrate": "prisma migrate dev --name test",
"db:generate": "prisma generate",
"dev": "tsx watch src/server.ts",
"test:integration": "pnpm db:up && sleep 5 && pnpm db:migrate && pnpm db:generate && vitest run --coverage",
"test:ui": "vitest --ui",
"test:watch": "vitest"
},
"devDependencies": {
"@prisma/client": "^5.14.0",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/node": "^22.4.1",
"@vitest/coverage-istanbul": "^2.0.4",
"@vitest/ui": "^2.0.4",
"jsdom": "^24.0.0",
"prisma": "^5.14.0",
"supertest": "^7.0.0",
"tsx": "^4.7.2",
"typescript": "^5.5.3",
"vitest": "^2.0.4"
},
"dependencies": {
"cors": "^2.8.5",
"express": "^4.19.2"
}
}Scripts automatisent workflow : pnpm test:integration lance DB+migration+tests+coverage. Ajoutez à CI (GitHub Actions) pour runs parallèles. Coverage >90% visé.
Bonnes pratiques
- Isolation stricte : Toujours cleanup avant/after chaque test (deleteMany ou transactions avec $transaction).
- Parallel safe : Limitez concurrency à capacité DB ; utilisez pools Prisma.
- Coverage thresholds : Configurez Vitest
coverage: { thresholds: { lines: 90 } }pour CI gates. - Mock externes : MSW pour APIs tierces, gardez DB réelle pour intégration.
- Seed data : Utilisez Prisma Studio ou factories (ex: Faker.js) pour datasets réalistes.
Erreurs courantes à éviter
- Pas de cleanup : Tests flaky dus à états persistants ; toujours reset DB.
- Connexions Prisma leak : Appel
$disconnect()afterEach, ou utilisezPrismaClientpar test. - Ports conflits : DB test sur port dédié (5433) ; healthcheck avant tests.
- Async non await : Supertest promises ; utilisez
async/awaitpartout pour éviter race conditions.
Pour aller plus loin
- Intégrez Playwright pour E2E + intégration.
- Explorez Vitest persistance pour DB SQLite in-memory (faster).
- CI/CD : GitHub Actions matrix pour Node versions.
- Ressources : Docs Vitest, Prisma Testing.