08 - Snapshot testing : quand c'est utile et quand c'est un piège
Ce que tu vas apprendre
- Comment fonctionne le snapshot testing
- Les bons cas d'usage (HTML, configs, structures JSON)
- Les pièges classiques et comment les éviter
toMatchSnapshot()ettoMatchInlineSnapshot()avec Bun
Prerequisites
Avoir lu l'article 02 - Tests unitaires.
Le principe
Le snapshot testing, c'est simple : tu exécutés ton code, tu captures la sortie, tu la sauvegardes dans un fichier. Au prochain bun test, la sortie est comparee avec le snapshot. Si ca a change, le test echoue.
C'est un filet de sécurité automatique. Tu n'ecris pas les assertions a la main, c'est le framework qui les généré pour toi.
typescriptimport { describe, it, expect } from "bun:test";
it("generates email HTML", () => {
const html = renderOrderConfirmation({
orderId: "ORD-001",
total: 42,
items: [{ product: "Widget", qty: 2, price: 21 }],
});
expect(html).toMatchSnapshot();
});
La première fois que tu lances ce test, Bun créé un fichier snapshot (dans un dossier __snapshots__) avec le contenu exact de html. Les fois suivantes, il compare. Si le HTML change, le test echoue et te montre le diff.
Mettre à jour les snapshots
Quand tu changes volontairement le rendu (nouveau template, nouveau format), les snapshots deviennent obsolètes. Pour les mettre à jour :
bashbun test --update-snapshots
Ca régénéré tous les snapshots. Attention : vérifié le diff dans git avant de committer. C'est la qu'est le piège (on y revient plus bas).
Les bons cas d'usage
Rendu HTML
Tu générés du HTML cote serveur (emails, pages statiques, composants) ? Le snapshot est parfait. Tu verifies que le HTML ne change pas par accident.
typescriptimport { describe, it, expect } from "bun:test";
import { renderArticleCard } from "./components/article-card";
describe("ArticleCard", () => {
it("renders with title and date", () => {
const html = renderArticleCard({
title: "Mon article",
date: "2026-03-28",
slug: "mon-article",
});
expect(html).toMatchSnapshot();
});
it("renders with tags", () => {
const html = renderArticleCard({
title: "Avec des tags",
date: "2026-03-28",
slug: "avec-des-tags",
tags: ["TypeScript", "Bun"],
});
expect(html).toMatchSnapshot();
});
});
Sur paltemps.fr, j'utilise ca pour le generateur de blog statique. Chaque modification de template est capturee.
Sorties CLI
Si tu développés un outil en ligne de commande, les snapshots verifient que l'output ne change pas :
typescriptit("formats help output", () => {
const output = formatHelp({
commands: ["build", "serve", "deploy"],
version: "1.0.0",
});
expect(output).toMatchSnapshot();
});
Structures de configuration
Quand tu générés des configs (Docker, nginx, CI), les snapshots attrapent les regressions :
typescriptit("generates docker-compose config", () => {
const config = generateDockerCompose({
services: ["api", "worker", "db"],
environment: "staging",
});
expect(config).toMatchSnapshot();
});
Shape de réponses API
Complement aux tests de contrat de l'article 07 : tu peux snapshotter la structure d'une réponse.
typescriptit("API response has stable shape", async () => {
const res = await app.handle(new Request("http://localhost/api/articles"));
const data = await res.json();
// On snapshot la structure, pas les valeurs
const shape = data.map((article: any) => Object.keys(article).sort());
expect(shape[0]).toMatchSnapshot();
});
Inline snapshots
Au lieu de stocker le snapshot dans un fichier séparé, tu peux l'inliner directement dans le test :
typescriptit("formats price", () => {
expect(formatPrice(4299)).toMatchInlineSnapshot(`"42,99 EUR"`);
});
it("formats empty cart", () => {
expect(formatCartSummary([])).toMatchInlineSnapshot(`"Panier vide"`);
});
L'inline snapshot est écrit directement dans le code source du test. Quand tu lances bun test --update-snapshots, Bun met à jour la string dans le fichier.
J'utilise les inline snapshots pour les petites valeurs (une ligne, un chiffre). Pour les gros blocs HTML ou JSON, le fichier externe est plus lisible.
Les pièges
Piege 1 : snapshotter des objets trop gros
typescript// MAUVAIS : snapshot d'un objet entier de 200 lignes
it("returns user profile", async () => {
const profile = await getUserProfile("user-123");
expect(profile).toMatchSnapshot(); // 200 lignes de JSON
});
Quand ce test echoue, le diff fait 30 lignes. Personne ne le lit. Tout le monde tape --update-snapshots sans vérifier. Le snapshot ne protégé plus rien.
Mieux : assertions explicites sur les champs qui comptent :
typescriptit("returns user profile", async () => {
const profile = await getUserProfile("user-123");
expect(profile.name).toBe("Alice");
expect(profile.email).toBe("alice@example.com");
expect(profile.role).toBe("admin");
});
Piege 2 : des valeurs qui changent a chaque run
typescript// MAUVAIS : le timestamp change a chaque execution
it("creates order", () => {
const order = Order.create([{ product: "W", qty: 1, price: 10 }]);
expect(order).toMatchSnapshot();
// Le snapshot contient createdAt: "2026-03-28T14:32:01.123Z"
// Au prochain run : "2026-03-28T14:32:05.456Z" => FAIL
});
Si ton objet contient des dates, des UUIDs ou des valeurs aleatoires, le snapshot casse a chaque run.
Solutions :
typescript// Option 1 : fixer l'horloge
import { spyOn } from "bun:test";
spyOn(Date, "now").mockReturnValue(1700000000000);
// Option 2 : snapshot seulement les champs stables
expect({ status: order.status, total: order.total }).toMatchSnapshot();
// Option 3 : serializer custom qui masque les valeurs instables
expect(sanitize(order)).toMatchSnapshot();
Piege 3 : le reflexe "update all"
Le pire ennemi du snapshot testing, c'est le dev qui lance --update-snapshots sans regarder le diff. J'en ai ete coupable moi-meme. Tu as 12 snapshots qui cassent, c'est vendredi, tu mets à jour et tu push.
Le lendemain, tu realises qu'un des 12 changements etait un vrai bug.
Regle : avant de committer un update de snapshot, regarde le git diff. Chaque changement de snapshot doit etre intentionnel.
Quand utiliser et quand éviter
| Situation | Snapshot ? | Pourquoi |
|---|---|---|
| Rendu HTML d'un template | Oui | Stable, humainement lisible |
| Sortie CLI formatee | Oui | Detecte les changements de format |
| Config générée | Oui | Evite les regressions |
| Objet métier complet | Non | Trop de champs, diff illisible |
| Réponse API avec timestamps | Non | Change a chaque run |
| Donnees de DB | Non | Instable (IDs, dates) |
Mon ratio perso : environ 10% de mes tests sont des snapshots. Le reste, c'est des assertions explicites. Le snapshot est un outil complémentaire, pas un remplacement.
Article précédent : 07 - Tests de contrat d'API Article suivant : 09 - Glossaire