Tests fondamentaux - 08 - Snapshot testing : quand c'est utile et quand c'est un piège

Le snapshot testing avec bun test : quand l'utiliser pour du HTML, du JSON ou des configs, et quand ca devient un piège.

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() et toMatchInlineSnapshot() 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

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.