Tests d'Intégration API : Stratégies, Outils et Bonnes Pratiques
Introduction
Les tests unitaires vérifient les fonctions individuelles. Les tests API vérifient les endpoints individuels. Mais ni l'un ni l'autre ne vous dit si vos services fonctionnent réellement ensemble. C'est le rôle des tests d'intégration.
Les tests d'intégration API valident que plusieurs services, bases de données et systèmes externes communiquent correctement via leurs interfaces API. Dans une architecture microservices où des dizaines de services dépendent les uns des autres, les tests d'intégration constituent le filet de sécurité qui détecte les contrats non concordants, les défaillances réseau et les problèmes de sérialisation des données.
Ce guide couvre les stratégies, les outils, des exemples de code et les bonnes pratiques nécessaires pour construire des tests d'intégration API fiables, des scénarios simples à deux services jusqu'aux flux de travail complexes multi-services.
Qu'est-ce que le Test d'Intégration API ?
Le test d'intégration API vérifie que les systèmes connectés fonctionnent correctement ensemble via leurs interfaces API. Contrairement aux tests unitaires (qui testent le code en isolation) ou aux tests de bout en bout (qui testent l'ensemble du système via l'interface utilisateur), les tests d'intégration se concentrent sur les frontières entre les services.
Ce que Valident les Tests d'Intégration
- Les données circulent correctement entre les services (le Service A envoie des données que le Service B peut analyser)
- Les contrats sont respectés (l'API renvoie les champs et types que le consommateur attend)
- La gestion des erreurs fonctionne au-delà des frontières de services (le Service A gère correctement les erreurs du Service B)
- L'authentification se propage correctement dans la chaîne de services
- Les interactions avec la base de données fonctionnent correctement (requêtes, transactions, migrations)
- Les API externes se comportent comme prévu (passerelles de paiement, services d'e-mail, données tierces)
Tests d'Intégration vs Autres Types de Tests
| Type de Test | Portée | Vitesse | Dépendances | Détecte |
|---|---|---|---|---|
| Tests Unitaires | Fonction/classe unique | Rapide (ms) | Simulées | Bugs logiques |
| Tests API | Endpoint unique | Rapide (ms-s) | Souvent simulées | Violations de contrat API |
| Tests d'Intégration | Plusieurs services | Moyen (s) | Réelles ou conteneurs | Non-correspondances d'interfaces |
| Tests E2E | Système complet + UI | Lent (min) | Toutes réelles | Bugs de flux utilisateur |
Stratégies de Tests d'Intégration API
Stratégie 1 : Tests d'Intégration Big Bang
Connectez tous les services à la fois et testez le système complet. Simple à comprendre mais difficile à déboguer en cas d'échec des tests.
Idéal pour : Les petits systèmes avec peu de services.
Stratégie 2 : Tests d'Intégration Incrémentaux
Ajoutez et testez les services un à la fois. Commencez par le service principal, puis ajoutez les services connectés de manière incrémentale.
De haut en bas (Top-down) : Démarrez avec la passerelle API et simulez les services en aval, puis remplacez les simulations par des services réels un par un.
De bas en haut (Bottom-up) : Démarrez avec les services de plus bas niveau (base de données, cache) et construisez vers le haut.
Sandwich : Combinez les approches top-down et bottom-up en vous rejoignant au milieu.
Idéal pour : Les systèmes de taille moyenne à grande où vous devez isoler les défaillances.
Stratégie 3 : Tests de Contrat (Contract Testing)
Définissez des contrats entre le consommateur et le fournisseur, puis vérifiez chaque côté indépendamment. C'est l'approche la plus évolutive pour les microservices.
Idéal pour : Les architectures microservices avec de nombreuses dépendances inter-services.
Tests d'Intégration API Pratiques avec du Code
JavaScript : Test d'Intégration de Service avec Supertest
// tests/integration/orders.test.js const request = require('supertest'); const app = require('../../src/app'); const db = require('../../src/db');describe('Orders API Integration', () => { let userId; let productId;
beforeAll(async () => { // Seed database with test data await db.migrate.latest(); const user = await db('users').insert({ name: 'Test User', email: 'test@example.com' }).returning('id'); userId = user[0].id;
const product = await db('products').insert({ name: 'Widget', price: 29.99, stock: 100 }).returning('id'); productId = product[0].id;});
afterAll(async () => { await db('orders').del(); await db('products').del(); await db('users').del(); await db.destroy(); });
test('Creating an order updates product stock', async () => { // Create order via API const orderRes = await request(app) .post('/api/orders') .send({ userId, items: [{ productId, quantity: 3 }] }) .expect(201);
expect(orderRes.body.total).toBe(89.97); // 29.99 * 3 // Verify stock was decremented const productRes = await request(app) .get(`/api/products/${productId}`) .expect(200); expect(productRes.body.stock).toBe(97); // 100 - 3});
test('Order fails when insufficient stock', async () => { const res = await request(app) .post('/api/orders') .send({ userId, items: [{ productId, quantity: 9999 }] }) .expect(400);
expect(res.body.error).toContain('Insufficient stock');});
test('Order creation sends notification to user service', async () => { const orderRes = await request(app) .post('/api/orders') .send({ userId, items: [{ productId, quantity: 1 }] }) .expect(201);
// Verify notification was created const notifRes = await request(app) .get(`/api/users/${userId}/notifications`) .expect(200); const orderNotif = notifRes.body.find( n => n.type === 'order_confirmation' ); expect(orderNotif).toBeDefined(); expect(orderNotif.orderId).toBe(orderRes.body.id);
}); });
Python : Test avec pytest et Docker
# tests/integration/test_order_flow.py import pytest import requests import timeAPI_URL = "http://localhost:3000/api"
@pytest.fixture(scope="module") def test_user(): """Create a test user and return their data.""" response = requests.post(f"{API_URL}/users", json={ "name": "Integration Test User", "email": "integration@test.com" }) assert response.status_code == 201 yield response.json() # Cleanup requests.delete(f"{API_URL}/users/{response.json()['id']}")
@pytest.fixture(scope="module") def test_product(): """Create a test product.""" response = requests.post(f"{API_URL}/products", json={ "name": "Test Widget", "price": 19.99, "stock": 50 }) assert response.status_code == 201 yield response.json() requests.delete(f"{API_URL}/products/{response.json()['id']}")
class TestOrderIntegration: def test_complete_order_flow(self, test_user, test_product): """Test the full order lifecycle across services.""" # Step 1: Create order order_response = requests.post(f"{API_URL}/orders", json={ "userId": test_user["id"], "items": [{"productId": test_product["id"], "quantity": 2}] }) assert order_response.status_code == 201 order = order_response.json() assert order["total"] == 39.98 # 19.99 * 2
# Step 2: Verify payment was processed payment_response = requests.get( f"{API_URL}/orders/{order['id']}/payment" ) assert payment_response.status_code == 200 assert payment_response.json()["status"] == "completed" # Step 3: Verify inventory updated product_response = requests.get( f"{API_URL}/products/{test_product['id']}" ) assert product_response.status_code == 200 assert product_response.json()["stock"] == 48 # 50 - 2 def test_order_rollback_on_payment_failure(self, test_user, test_product): """Verify stock is restored when payment fails.""" initial_stock = requests.get( f"{API_URL}/products/{test_product['id']}" ).json()["stock"] # Create order with invalid payment method to trigger failure order_response = requests.post(f"{API_URL}/orders", json={ "userId": test_user["id"], "items": [{"productId": test_product["id"], "quantity": 1}], "paymentMethod": "invalid_card" }) assert order_response.status_code == 400 # Verify stock was not decremented current_stock = requests.get( f"{API_URL}/products/{test_product['id']}" ).json()["stock"] assert current_stock == initial_stock
Contract Testing avec Pact
Le contract testing est l'approche la plus efficace pour tester les intégrations API dans les microservices. Le consommateur définit ce qu'il attend du fournisseur, et les deux côtés vérifient indépendamment.
Test Côté Consommateur (JavaScript)
// consumer/tests/userServiceClient.pact.test.js const { PactV3 } = require('@pact-foundation/pact'); const { UserServiceClient } = require('../src/userServiceClient');const provider = new PactV3({ consumer: 'OrderService', provider: 'UserService', });
describe('UserService Client', () => { test('fetches user by ID', async () => { provider .given('a user with ID 1 exists') .uponReceiving('a request for user 1') .withRequest({ method: 'GET', path: '/api/users/1', headers: { Accept: 'application/json' }, }) .willRespondWith({ status: 200, headers: { 'Content-Type': 'application/json' }, body: { id: 1, name: 'John Doe', email: 'john@example.com', }, });
await provider.executeTest(async (mockServer) => { const client = new UserServiceClient(mockServer.url); const user = await client.getUser(1); expect(user.id).toBe(1); expect(user.name).toBe('John Doe'); });
}); });
Vérification Côté Fournisseur
// provider/tests/pactVerification.test.js const { Verifier } = require('@pact-foundation/pact');describe('UserService Provider Verification', () => { test('validates contract with OrderService', async () => { const verifier = new Verifier({ providerBaseUrl: 'http://localhost:3001', pactUrls: ['./pacts/OrderService-UserService.json'], stateHandlers: { 'a user with ID 1 exists': async () => { // Set up the required state in the provider await db('users').insert({ id: 1, name: 'John Doe', email: 'john@example.com', }); }, }, });
await verifier.verifyProvider();
}); });
Utilisation de Docker pour les Tests d'Intégration
Les tests d'intégration nécessitent des dépendances réelles (bases de données, caches, files de messages). Docker Compose rend cela gérable :
# docker-compose.test.yml version: '3.8' services: api: build: . environment: DATABASE_URL: postgres://test:test@db:5432/testdb REDIS_URL: redis://cache:6379 depends_on: db: condition: service_healthy cache: condition: service_starteddb: image: postgres:16 environment: POSTGRES_DB: testdb POSTGRES_USER: test POSTGRES_PASSWORD: test healthcheck: test: pg_isready -U test interval: 5s retries: 5
cache: image: redis:7-alpine
test-runner: build: context: . dockerfile: Dockerfile.test environment: API_URL: http://api:3000 depends_on: - api command: npm run test:integration
# Run integration tests
docker-compose -f docker-compose.test.yml up --build --abort-on-container-exit
Tests d'Intégration en CI/CD
# GitHub Actions integration tests
name: Integration Tests
on:
push:
branches: [main, develop]
jobs:
integration:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7-alpine
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run migrate
env:
DATABASE_URL: postgres://test:test@localhost:5432/testdb
- run: npm run test:integration
env:
DATABASE_URL: postgres://test:test@localhost:5432/testdb
REDIS_URL: redis://localhost:6379
Bonnes Pratiques pour les Tests d'Intégration API
1. Utilisez de Vraies Dépendances dans la Mesure du Possible
Les tests d'intégration avec des dépendances simulées ne sont pas de vrais tests d'intégration, ce sont des tests unitaires déguisés. Utilisez de vraies bases de données, de vrais caches et de vraies files de messages via des conteneurs Docker.
2. Isolez les Données de Test
Chaque test doit créer ses propres données et nettoyer après lui. Utilisez des transactions de base de données qui sont annulées après chaque test, ou tronquez les tables entre les exécutions de tests.
3. Testez les Scénarios d'Erreur
Ne testez pas uniquement le chemin heureux. Testez ce qui se passe lorsque les dépendances sont indisponibles, renvoient des erreurs ou des données inattendues.
4. Gardez les Tests Rapides
Les tests d'intégration sont plus lents que les tests unitaires, mais ils ne doivent pas être trop lents. Visez moins de 30 secondes pour votre suite d'intégration complète. Utilisez l'exécution parallèle et la configuration partagée là où c'est sûr.
5. Utilisez le Contract Testing pour les API Inter-Équipes
Lorsque différentes équipes possèdent différents services, le contract testing (avec Pact ou similaire) est plus pratique que d'exécuter tous les services ensemble.
6. Combinez avec d'Autres Types de Tests
Les tests d'intégration complètent les tests API REST, les tests de charge et les tests de sécurité. Utilisez Qodex.ai pour générer automatiquement des tests fonctionnels et de sécurité, puis ajoutez des tests d'intégration pour les flux de travail inter-services.
Pour une vue d'ensemble complète des outils de test, consultez notre comparatif des outils de test API.
Foire aux Questions
Quelle est la différence entre les tests API et les tests d'intégration API ?
Les tests API valident un endpoint API unique en isolation : codes de statut corrects, corps de réponse et gestion des erreurs. Les tests d'intégration API valident que plusieurs services fonctionnent ensemble via leurs API : les données circulent correctement, les contrats sont respectés et les erreurs se propagent correctement au-delà des frontières de services.
Les tests API sont-ils identiques aux tests d'intégration ?
Pas exactement. Les tests API peuvent être effectués en isolation (en simulant les dépendances), ce qui les rapproche des tests unitaires. Les tests d'intégration testent spécifiquement les interactions entre des services réels, des bases de données et des systèmes externes. Il y a cependant un chevauchement significatif, et de nombreuses équipes utilisent des tests API comme tests d'intégration lorsqu'ils sont effectués contre de vraies dépendances.
Comment tester l'intégration API sans accès au service réel ?
Utilisez le contract testing avec des outils comme Pact. Le consommateur définit les interactions attendues, et les deux côtés vérifient indépendamment. Vous pouvez également utiliser des serveurs simulés, WireMock ou des outils de virtualisation de service pour simuler le service externe.
Quels outils sont les meilleurs pour les tests d'intégration API ?
Pour JavaScript : Supertest + Jest avec Docker. Pour Python : pytest + requests avec Docker. Pour le contract testing : Pact. Pour la génération automatisée de tests : Qodex.ai. Pour les tests inter-services : Docker Compose pour orchestrer tous les services.
Comment gérer les données de test dans les tests d'intégration ?
Utilisez des transactions de base de données qui sont annulées après chaque test, ou tronquez les tables entre les exécutions de tests. Créez des données de test dans les hooks setUp/beforeEach et nettoyez dans tearDown/afterEach. Ne partagez jamais des données de test entre des tests ; chaque test doit être indépendant.
Les tests d'intégration doivent-ils s'exécuter en CI/CD ?
Oui. Les tests d'intégration doivent s'exécuter à chaque push et pull request. Utilisez Docker Compose ou des conteneurs de services CI/CD (comme les services GitHub Actions) pour démarrer de vraies dépendances. Gardez la suite rapide en vous concentrant sur les chemins d'intégration critiques et en exécutant les tests en parallèle.
Discover, Test, & Secure your APIs 10x Faster than before
Auto-discover every endpoint, generate functional & security tests (OWASP Top 10), auto-heal as code changes, and run in CI/CD - no code needed.
Related Blogs





