Test unitaire vs intégration
Un test unitaire vérifie une seule unité de code en isolation, en général en mockant ses dépendances. Un test d’intégration vérifie que plusieurs composants réels — base de données, API, autres classes — collaborent correctement ensemble.
Sommaire
Le problème #
Une équipe teste une fonction calculerRemise() en isolation : elle passe toujours. Pourtant, en production, la remise ne s’applique jamais correctement — parce que la requête SQL qui récupère le statut fidélité du client renvoie la mauvaise colonne. Le test unitaire n’a rien vu, car il ne testait que la logique de calcul, pas la façon dont les composants s’assemblent réellement.
À l’inverse, une suite composée uniquement de tests qui démarrent une vraie base de données et font de vrais appels HTTP devient si lente que personne ne la lance avant de pousser son code, et les retours d’échec arrivent des heures plus tard, noyés dans le bruit d’une pipeline CI. Le périmètre du test — ce qu’il isole et ce qu’il laisse réel — n’est jamais un détail anodin.
L’idée générale #
Un test unitaire vérifie une seule unité de code (une fonction, une méthode, une classe) en l’isolant complètement de ses dépendances. Toute dépendance externe — base de données, appel réseau, système de fichiers, horloge — est remplacée par un mock ou un stub. Le test unitaire répond à une question précise : cette logique, prise isolément, se comporte-t-elle comme prévu ?
Un test d’intégration vérifie que plusieurs composants réels collaborent correctement entre eux. Il peut faire appel à une vraie base de données (souvent une base de test dédiée), un vrai client HTTP contre un serveur de test, ou plusieurs classes réelles qui interagissent sans mock. Il répond à une autre question : ces composants, assemblés ensemble, produisent-ils le résultat attendu ?
Les deux ne s’opposent pas — ils couvrent des risques différents :
| Test unitaire | Test d’intégration | |
|---|---|---|
| Périmètre | Une seule unité | Plusieurs composants réels |
| Dépendances | Mockées / stubbées | Réelles (ou proches du réel) |
| Vitesse | Très rapide (millisecondes) | Plus lent (connexions réelles) |
| Détecte | Erreurs de logique isolée | Erreurs d’assemblage, de configuration, de contrat |
| Débogage en cas d’échec | Précis, cause évidente | Plus large, cause à identifier parmi plusieurs composants |
Analogie du quotidien #
Tester unitairement, c’est vérifier chaque pièce détachée d’un meuble en kit séparément : la vis tient-elle la charge prévue, la planche a-t-elle les bonnes dimensions. Tester en intégration, c’est monter réellement deux ou trois pièces ensemble et vérifier qu’elles s’assemblent sans jeu ni porte-à-faux.
Vérifier chaque vis isolément est rapide et précis, mais ne garantit pas que le meuble entier tiendra debout une fois monté — un plan de perçage mal aligné entre deux pièces, chacune parfaite individuellement, peut rendre l’assemblage impossible. C’est exactement ce qu’un test d’intégration détecte et qu’un test unitaire, par construction, ne peut pas voir.
Diagramme #
Exemple de code #
PHP #
// --- Code métier ---
interface ClientRepository
{
public function estFidele(int $clientId): bool;
}
final class ServiceRemise
{
public function __construct(private ClientRepository $repository) {}
public function calculer(int $clientId, float $montant): float
{
return $this->repository->estFidele($clientId)
? $montant * 0.9
: $montant;
}
}
use PHPUnit\Framework\TestCase;
// --- Test UNITAIRE : le repository est mocké, aucune vraie base de données ---
final class ServiceRemiseUnitTest extends TestCase
{
public function testClientFideleObtientUneRemise(): void
{
$repository = $this->createStub(ClientRepository::class);
$repository->method('estFidele')->willReturn(true);
$service = new ServiceRemise($repository);
$this->assertSame(90.0, $service->calculer(clientId: 1, montant: 100.0));
}
}
// --- Test D'INTÉGRATION : le repository réel interroge une vraie base de test ---
final class ServiceRemiseIntegrationTest extends TestCase
{
private PDO $pdo;
protected function setUp(): void
{
$this->pdo = new PDO('sqlite::memory:');
$this->pdo->exec('CREATE TABLE clients (id INTEGER, fidele INTEGER)');
$this->pdo->exec('INSERT INTO clients VALUES (1, 1)');
}
public function testCalculDeRemiseAvecUneVraieBaseDeDonnees(): void
{
$repository = new PdoClientRepository($this->pdo);
$service = new ServiceRemise($repository);
$this->assertSame(90.0, $service->calculer(clientId: 1, montant: 100.0));
}
}
JavaScript #
// --- Code métier ---
class ServiceRemise {
constructor(clientRepository) {
this.clientRepository = clientRepository;
}
async calculer(clientId, montant) {
const estFidele = await this.clientRepository.estFidele(clientId);
return estFidele ? montant * 0.9 : montant;
}
}
module.exports = { ServiceRemise };
// --- Test UNITAIRE : le repository est mocké avec Jest ---
const { ServiceRemise } = require("./serviceRemise");
test("un client fidèle obtient une remise (unitaire, repository mocké)", async () => {
const repositoryMocke = { estFidele: jest.fn().mockResolvedValue(true) };
const service = new ServiceRemise(repositoryMocke);
const total = await service.calculer(1, 100);
expect(total).toBe(90);
});
// --- Test D'INTÉGRATION : appel réel à une base SQLite en mémoire ---
const { ServiceRemise } = require("./serviceRemise");
const { SqlClientRepository } = require("./sqlClientRepository");
const { openTestDatabase } = require("./testDb");
test("un client fidèle obtient une remise (intégration, vraie base de données)", async () => {
const db = await openTestDatabase();
await db.run("INSERT INTO clients (id, fidele) VALUES (1, 1)");
const repository = new SqlClientRepository(db);
const service = new ServiceRemise(repository);
const total = await service.calculer(1, 100);
expect(total).toBe(90);
});
Quand utiliser l’un ou l’autre ? #
- Test unitaire : pour vérifier une logique métier isolée (calcul, validation, transformation) rapidement et souvent — à chaque sauvegarde ou en pré-commit.
- Test d’intégration : pour vérifier qu’une requête SQL est correcte, qu’un appel HTTP respecte le bon format, ou que plusieurs classes s’assemblent sans erreur de configuration.
- Une suite saine combine les deux : beaucoup de tests unitaires rapides pour la logique, un nombre plus restreint de tests d’intégration pour les points de jonction critiques.
- Évitez de mocker une dépendance dans un test qui a justement pour but de vérifier que cette dépendance fonctionne correctement en conditions réelles — ce serait un test d’intégration déguisé en test unitaire, qui ne teste plus rien d’utile.
Points importants #
- Un test unitaire rapide (millisecondes) peut tourner par centaines à chaque sauvegarde de fichier ; un test d’intégration, plus lent, tourne plus souvent en pré-push ou en CI.
- Un échec de test unitaire pointe précisément vers la logique cassée ; un échec de test d’intégration demande d’investiguer parmi plusieurs composants pour trouver la cause.
- Le périmètre d’un test (ce qu’il mocke, ce qu’il laisse réel) doit toujours être un choix conscient, pas un hasard d’implémentation.
- Ni l’un ni l’autre ne remplace les tests end-to-end, qui vérifient le système complet du point de vue de l’utilisateur final — les trois niveaux forment la pyramide des tests.
🐻 À retenir
- ●Un test unitaire isole une seule unité et mocke ses dépendances externes ; un test d’intégration vérifie que plusieurs composants réels collaborent correctement.
- ●Plus un test se rapproche de la réalité (vraie base de données, vrai réseau), plus il est lent et fragile — c’est le compromis central des tests d’intégration.
- ●Un test unitaire qui mocke tout répond à ‘ma logique est-elle correcte ?’ ; un test d’intégration répond à ‘mes composants s’assemblent-ils correctement ?’ — les deux questions sont différentes et complémentaires.