Introduction
Les tests d'intégration vérifient que vos composants (API, base de données, services) interagissent correctement, contrairement aux tests unitaires isolés. En 2026, avec des architectures microservices et des bases NoSQL/ProSQL hybrides, ils sont essentiels pour détecter les bugs subtils comme les fuites de transactions ou les race conditions. Ce tutoriel avancé se concentre sur une API Express avec Prisma (SQLite pour simplicité), testée via Jest et Supertest. Vous apprendrez à utiliser des transactions Prisma pour l'isolation parfaite des tests, des mocks partiels pour les dépendances externes, et des assertions sur les headers/JSON. À la fin, vos tests s'exécuteront en <500ms par suite, avec 100% de coverage critique. Parfait pour les équipes scalant vers CI/CD avancés comme GitHub Actions avec matrices parallèles.
Prérequis
- Node.js 20+ installé
- Connaissances avancées en TypeScript, Express et Prisma
- Familiarité avec Jest et les patterns async/await
- SQLite activé (pas de config externe requise)
Initialisation du projet et installation
mkdir api-tests-integration
cd api-tests-integration
npm init -y
npm install express prisma @prisma/client typescript ts-node @types/express @types/node
npm install -D jest supertest @types/supertest ts-jest @types/jest prisma
npx prisma init --datasource-provider sqlite
tsc --init
npm pkg set type=moduleCe script initialise un projet Node.js moderne avec TypeScript, Express pour l'API, Prisma pour l'ORM et Jest/Supertest pour les tests. Le flag --datasource-provider sqlite configure Prisma pour SQLite in-memory, idéal pour des tests rapides sans DB persistante. Évitez npm install -g pour la reproductibilité en CI.
Schéma Prisma avec modèle User
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
name String
email String @unique
createdAt DateTime @default(now())
}
model Account {
id String @id @default(cuid())
userId Int
user User @relation(fields: [userId], references: [id])
}Ce schéma définit un modèle User simple avec relation vers Account pour simuler de la complexité. L'URL DB sera surchargée en tests pour :memory:. Utilisez @unique sur email pour tester les contraintes DB réelles, piège courant : oublier les relations cascade en delete.
Serveur Express avec routes CRUD
import express from 'express';
import { PrismaClient } from '@prisma/client';
const app = express();
const prisma = new PrismaClient();
app.use(express.json());
app.get('/users', async (req, res) => {
const users = await prisma.user.findMany({ include: { accounts: true } });
res.json(users);
});
app.post('/users', async (req, res) => {
try {
const { name, email } = req.body;
const user = await prisma.user.create({
data: { name, email },
include: { accounts: true }
});
res.status(201).json(user);
} catch (error: any) {
res.status(400).json({ error: error.message });
}
});
app.delete('/users/:id', async (req, res) => {
const { id } = req.params;
await prisma.user.delete({ where: { id: parseInt(id) } });
res.status(204).send();
});
const PORT = 3001;
app.listen(PORT, () => console.log(`Server on port ${PORT}`));Ce serveur expose des routes CRUD basiques avec include pour tester les relations Prisma. Les erreurs sont gérées pour simuler des contraintes DB. Piège : sans try/catch, les erreurs Prisma crashent le process ; toujours logger en prod mais omettre en tests pour focus isolation.
Configuration Jest pour tests TypeScript
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/tests/'],
testMatch: ['**/*.test.ts'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
],
coverageThreshold: {
global: {
branches: 90,
functions: 90,
lines: 90,
statements: 90
}
},
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
maxWorkers: '50%'
};
export default config;Cette config optimise pour TypeScript avec coverage strict (90% min), workers parallèles pour vitesse, et setup global. Le setupFilesAfterEnv gérera Prisma. Piège : sans roots, Jest scanne tout ; limitez pour <1s cold start.
Setup global pour Prisma en tests
import { PrismaClient } from '@prisma/client';
const globalSetups: Array<() => Promise<unknown>> = [];
beforeAll(async () => {
globalSetups.forEach(setup => setup());
});
afterAll(async () => {
await new Promise<void>((resolve) => setImmediate(resolve));
});
// Global Prisma pour transactions
globalThis.prisma = globalThis.prisma || new PrismaClient();Ce setup prépare un PrismaClient global pour transactions partagées si besoin, avec setImmediate pour flush async. Utilisez globalThis pour access cross-tests. Piège : sans afterAll, les connexions fuient ; toujours closer en CI.
Test GET /users avec isolation transactionnelle
import request from 'supertest';
import { prisma } from '../../src/server';
import type { User } from '@prisma/client';
const app = (await import('../../src/server')).default;
describe('GET /users', () => {
let txClient: any;
beforeEach(async () => {
txClient = await prisma.$transaction(async (tx) => {
await tx.user.deleteMany({});
const user1: User = await tx.user.create({ data: { name: 'Alice', email: 'alice@test.com' } });
const user2: User = await tx.user.create({ data: { name: 'Bob', email: 'bob@test.com' } });
return { tx, user1, user2 };
});
});
afterEach(async () => {
await txClient.tx.$rollback();
});
it('devrait retourner la liste des users avec relations', async () => {
const res = await request(app).get('/users');
expect(res.status).toBe(200);
expect(res.body).toHaveLength(2);
expect(res.body[0].name).toBe('Alice');
expect(res.body[0].accounts).toEqual([]);
});
it('devrait avoir les bons headers', async () => {
const res = await request(app).get('/users');
expect(res.headers['content-type']).toMatch(/json/);
});
});Ce test utilise $transaction + $rollback pour isolation parfaite : seed avant, reset après, sans pollution. Assertions sur body, length, headers. Piège : sans rollback explicite, données persistent ; testez avec prisma.$disconnect() en afterAll pour prod-like.
Test POST /users avec validation et erreur
import request from 'supertest';
import { prisma } from '../../src/server';
import type { User } from '@prisma/client';
const app = (await import('../../src/server')).default;
describe('POST /users', () => {
beforeAll(async () => {
await prisma.user.deleteMany({});
});
it('devrait créer un user valide', async () => {
const newUser = { name: 'Charlie', email: 'charlie@test.com' };
const res = await request(app)
.post('/users')
.send(newUser)
.expect(201);
expect(res.body.email).toBe('charlie@test.com');
expect(res.body.createdAt).toBeDefined();
});
it('devrait rejeter un email dupliqué', async () => {
await request(app)
.post('/users')
.send({ name: 'Dup', email: 'charlie@test.com' })
.expect(400);
});
});Teste création + erreur DB (unique constraint) avec .expect(status). beforeAll nettoie globalement. Advanced : chain .send() + assertions post-réponse. Piège : order dépendant ; utilisez transactions par test pour indépendance.
Test DELETE avec mock partiel externe
import request from 'supertest';
import { prisma } from '../../src/server';
const app = (await import('../../src/server')).default;
jest.mock('../../src/server', () => ({
default: app
}));
describe('DELETE /users/:id', () => {
let userId: number;
beforeEach(async () => {
const tx = await prisma.$transaction(async (tx) => {
const user = await tx.user.create({ data: { name: 'DeleteMe', email: `del${Date.now()}@test.com` } });
return user.id;
});
userId = tx;
});
afterEach(async () => {
await prisma.$transaction(async (tx) => tx.user.deleteMany({}));
});
it('devrait supprimer un user existant', async () => {
const res = await request(app).delete(`/users/${userId}`).expect(204);
const users = await prisma.user.findMany();
expect(users).toHaveLength(0);
});
});Mock partiel du server pour dynamic import, teste delete avec vérif post-DB. Utilise id dynamique pour unicité. Piège : sans afterEach transactionnel, états persistent ; mockez seulement si externe (ex: logger), pas le SUT.
Bonnes pratiques
- Isolation transactionnelle : Toujours utiliser
prisma.$transaction+ rollback pour zéro état partagé. - Coverage ciblée : Visez 90% sur branches critiques (erreurs, edges), pas 100% triviaux.
- Parallélisme :
maxWorkers: '50%'+ DB :memory: pour <100ms/test en CI. - Assertions précises : Vérifiez headers, status, schema JSON avec
toMatchObject. - Cleanup global :
prisma.$disconnect()en afterAll pour éviter fuites connexions.
Erreurs courantes à éviter
- Oublier le rollback : Tests mutent la DB globalement, flaky en parallèle.
- Dynamic ports non gérés : Fixez PORT=3001, mockez
process.envsi besoin. - Assertions trop lâches :
expect(res.body).toBeDefined()au lieu detoMatchObject({name: 'exact'}). - Pas de seed transactionnel : Créations manuelles polluent les suites suivantes.
Pour aller plus loin
- Intégrez Testcontainers pour Postgres réel : Docs Docker.
- Passez à Vitest pour Vite-speed : Migration guide.
- CI/CD avec coverage badges : GitHub Actions matrix.