Guides
Tester les emails avec Bun, Vitest et Playwright
Utilisez des configurations de test, des assertions natives, des rapports avec données sensibles masquées et des pannes SMTP avec Bun test, Vitest et Playwright.InboxTap fournit des configurations de test optionnelles et natives pour Bun test, Vitest et Playwright. Chaque configuration démarre les écouteurs SMTP et HTTP locaux sur des ports sélectionnés automatiquement, crée un transport Nodemailer prêt à l’emploi et garantit le nettoyage.
Installer les dépendances optionnelles
Installez uniquement l’adaptateur de l’outil d’exécution utilisé par votre projet. Nodemailer 9 est requis par toutes les configurations ; Vitest 4.1 et Playwright 1.61 sont des dépendances optionnelles de leurs sous-chemins respectifs.
bun add --dev inboxtap nodemailer
bun add --dev vitest
# or
bun add --dev @playwright/testLes dépendances des configurations de test restent derrière des sous-chemins de paquet isolés. Importer
inboxtap ou inboxtap/client ne nécessite ni Nodemailer, ni Vitest, ni Playwright.
Configuration de test partagée
Utilisez startInboxTapFixture() lorsqu’un outil d’exécution possède son propre modèle de cycle de vie. Les
deux ports valent 0 par défaut : le système d’exploitation choisit donc des ports libres. Le
démarrage vérifie le transport Nodemailer avant de rendre la main, nettoie les échecs partiels et
close() peut être appelé plusieurs fois sans risque.
import { startInboxTapFixture } from "inboxtap/fixtures";
const inboxTap = await startInboxTapFixture();
try {
const inbox = await inboxTap.createInbox({ alias: "signup" });
await inboxTap.transport.sendMail({
from: "app@local.test",
to: inbox.address,
subject: "Verify your account",
text: "Open https://app.local.test/verify?id=example",
});
await inbox.waitForMessage({ subject: /verify your account/i });
} finally {
await inboxTap.close();
}L’objet renvoyé expose aussi server, client et les informations de connexion smtp pour les
applications qui doivent démarrer avec les ports sélectionnés.
Bun test
setupInboxTap() enregistre les fonctions asynchrones de configuration et de nettoyage de Bun. Bun
n’injecte pas de contexte de test personnalisé : créez donc explicitement une nouvelle boîte
dans chaque test.
import { expect, test } from "bun:test";
import { setupInboxTap } from "inboxtap/fixtures/bun";
const inboxTap = setupInboxTap();
test("captures one email", async () => {
const inbox = await inboxTap.createInbox({ alias: "signup" });
await inboxTap.transport.sendMail({
from: "app@local.test",
to: inbox.address,
subject: "Verify your account",
text: "Open https://app.local.test/verify?id=example",
});
const message = await inbox.waitForMessage({ subject: /verify your account/i });
expect(message.envelope.to).toContain(inbox.address);
});Vitest
extendInboxTap() ajoute à un test de base Vitest un contexte inboxTap à portée fichier et un
contexte inbox à portée test. Chaque test reçoit une nouvelle adresse tandis que le fichier
réutilise un seul serveur et un transport vérifiés.
import { expect, test as base } from "vitest";
import { extendInboxTap } from "inboxtap/fixtures/vitest";
const test = extendInboxTap(base);
test("captures one email", async ({ inboxTap, inbox }) => {
await inboxTap.transport.sendMail({
from: "app@local.test",
to: inbox.address,
subject: "Verify your account",
text: "Open https://app.local.test/verify?id=example",
});
const message = await inbox.waitForMessage({ subject: /verify your account/i });
expect(message.envelope.to).toContain(inbox.address);
});Playwright
L’adaptateur Playwright fournit un contexte inboxTap à portée processus et un contexte inbox à
portée test. Chaque processus reçoit ses propres ports dynamiques et chaque test sa propre adresse
destinataire.
import { expect, test as base } from "@playwright/test";
import { extendInboxTap } from "inboxtap/fixtures/playwright";
const test = extendInboxTap(base);
test("captures a verification link", async ({ inboxTap, inbox }) => {
await inboxTap.transport.sendMail({
from: "app@local.test",
to: inbox.address,
subject: "Verify your account",
text: "Open https://app.local.test/verify?id=example",
});
const link = await inbox.waitForLink({ subject: /verify your account/i });
expect(link).toContain("/verify");
});Si l’application a besoin du port SMTP dynamique, démarrez-la dans un autre contexte à portée processus qui
dépend de inboxTap, puis lisez inboxTap.smtp au lancement du processus. Le webServer de
Playwright démarre avant les contextes de test : un webServer déjà démarré ne peut donc pas
consommer un port sélectionné plus tard par le contexte InboxTap à portée processus.
Assertions natives des outils d’exécution
InboxTap conserve les implémentations des assertions dans le sous-chemin sans dépendance optionnelle
inboxtap/matchers et publie des adaptateurs typés pour chaque outil d’exécution. Injectez l’expect
de l’outil dans extendInboxTapExpect(). Bun et Vitest étendent cet objet sur place :
import { expect } from "vitest";
import { extendInboxTapExpect } from "inboxtap/matchers/vitest";
extendInboxTapExpect(expect);La configuration Bun suit le même modèle avec bun:test et inboxtap/matchers/bun.
L’expect.extend() natif de Playwright renvoie un nouvel objet : exportez donc la valeur renvoyée
avec votre contexte de test étendu.
import { expect as baseExpect, test as baseTest } from "@playwright/test";
import { extendInboxTap } from "inboxtap/fixtures/playwright";
import { extendInboxTapExpect } from "inboxtap/matchers/playwright";
export const test = extendInboxTap(baseTest);
export const expect = extendInboxTapExpect(baseExpect);Les assertions s’utilisent avec la boîte propre au test et les messages capturés :
await expect(inbox).toHaveDeliveredOnce({
subject: /verify your account/i,
quietMs: 100,
});
const email = await inbox.waitForMessage({ subject: /verify your account/i });
expect(email).toHaveRecipient(inbox.address);
expect(email).toContainLink("/verify");
expect(email).toHaveUnsubscribeHeader({ oneClick: true });toHaveDeliveredOnce() est une assertion sur un instantané : elle inspecte les messages déjà
capturés et n’attend pas la première livraison. quietMs est une fenêtre d’observation facultative
qui ne commence que si l’instantané initial contient exactement un message correspondant. Elle
peut détecter une nouvelle tentative pendant cette fenêtre, mais ne prouve pas qu’aucune tentative
plus tardive n’arrivera.
Utilisez directement inboxTapMatchers avec un autre expect compatible de style Jest, ou créez
un nouvel ensemble via createInboxTapMatchers({ recorder }). Ces exports n’importent aucun
outil d’exécution. Le collecteur reçoit des observations structurées sans contenu sensible ; les diagnostics
des assertions omettent également les corps, les valeurs de destinataire, les liens, les motifs
porteurs de jetons et les en-têtes bruts.
Écrire un rapport de test avec les données sensibles masquées
InboxTapReport, depuis inboxtap/reports, recueille à la fois les observations des assertions,
les messages capturés et les assertions de l’application. Pour un flux de rapport Vitest non
concurrent, étendez l’expect lié au test en cours :
import { test as base } from "vitest";
import { extendInboxTap } from "inboxtap/fixtures/vitest";
import { extendInboxTapExpect } from "inboxtap/matchers/vitest";
import { InboxTapReport } from "inboxtap/reports";
const test = extendInboxTap(base);
test("writes redacted evidence", async ({ expect, inboxTap, inbox }) => {
const report = new InboxTapReport({ title: "Signup email" });
extendInboxTapExpect(expect, { recorder: report });
try {
await inboxTap.transport.sendMail({
from: "app@local.test",
to: inbox.address,
subject: "Verify your account",
text: "Open https://app.local.test/verify/id-example?next=private",
});
await expect(inbox).toHaveDeliveredOnce({ subject: /verify/i });
const email = await inbox.waitForMessage({ subject: /verify/i });
report.addAssertion({
name: "verification email exposes one link",
passed: email.links.length === 1,
messageId: email.id,
});
} finally {
for (const email of await inbox.messages()) report.addMessage(email);
await report.write("artifacts/signup-email.json");
await report.write("artifacts/signup-email.html");
}
});L’écriture depuis finally conserve les preuves les plus récentes lorsqu’une assertion ou une
assertion de l’application échoue.
Les mêmes entrées ordonnées produisent un JSON déterministe et versionné ou un HTML statique
autonome. write() déduit le format de .json ou .html et crée les répertoires parents
manquants. Le rapport exclut par défaut la source RFC brute, attribue des pseudonymes cohérents aux
adresses e-mail, masque les surfaces secrètes courantes, échappe le HTML capturé et ne charge jamais
de ressources distantes. La collecte s’arrête à 100 messages et 1 000 assertions ; chaque artefact
rendu est limité à 10 Mio et consigne explicitement la troncature. Le comptage des octets omis
distingue les valeurs exactes des limites inférieures mesurées.
La portée du collecteur suit l’expect étendu. Ne rattachez pas de manière répétée des collecteurs
propres aux tests à un même expect Bun ou Vitest partagé pendant une exécution concurrente ;
enregistrez explicitement les messages et les assertions de l’application, ou produisez
délibérément un seul artefact pour la suite. Playwright peut créer un nouvel expect renvoyé pour
chaque rapport. Le masquage reste une protection non exhaustive : vérifiez les artefacts avant de les
partager. includeRaw: true est une option plus risquée, car elle conserve une copie de la source
RFC brute dont les données sensibles ne sont masquées que de manière non exhaustive.
Isolation et nettoyage
Ne partagez le serveur qu’à la portée définie par l’adaptateur. Ne créez jamais un seul TestInbox
global pour toute la suite : appelez createInbox() dans chaque test Bun, ou utilisez l’inbox
injectée dans Vitest et Playwright. Des destinataires d’enveloppe uniques isolent les tests
concurrents sans effacer les messages d’un autre test.
Tous les adaptateurs arrêtent le serveur et ferment le transport à la fin de leur portée native.
Vous pouvez passer des options serveur à setupInboxTap(options) ou
extendInboxTap(baseTest, options) si les valeurs par défaut doivent être adaptées ; les ports SMTP
et API omis restent dynamiques.
Tester les chemins d’échec
Utilisez le contrôleur de pannes du serveur de la configuration de test pour tester les nouvelles tentatives de l’application à la vraie frontière SMTP. Ciblez chaque règle sur le destinataire d’enveloppe unique du test afin qu’une transaction concurrente ne puisse pas la consommer. Enregistrez la règle juste avant de déclencher l’application.
test("retries a transient SMTP failure", async ({ inboxTap, inbox }) => {
const send = () =>
inboxTap.transport.sendMail({
from: "app@local.test",
to: inbox.address,
subject: "Verify your account",
text: "Open https://app.local.test/verify?id=example",
});
inboxTap.server.faults.failNext({
code: 451,
to: inbox.address,
});
await expect(send()).rejects.toThrow();
await expect(send()).resolves.toBeDefined();
expect(await inbox.messages()).toHaveLength(1);
});Une réponse 451 convient aux chemins de nouvelle tentative et d’attente progressive ; utilisez une réponse
550 pour le traitement des échecs permanents. delayNext() teste les délais d’expiration de l’application, et
disconnectNext() la récupération après une session SMTP interrompue. Les tentatives échouées ou
déconnectées ne produisent jamais de message capturé partiel.
Pour la concurrence, utilisez pauseNext() comme barrière explicite au lieu d’une temporisation.
Attendez gate.waitUntilPaused() avant de lancer l’action concurrente et appelez
gate.release(), idempotente, dans un bloc finally. Le délai d’expiration absolu de 60 secondes par défaut
borne toujours une correspondance ou une libération manquée ; si une pause active expire, SMTP
renvoie 451 et le message n’est pas capturé. Le nettoyage abandonne les barrières restantes ; évitez
d’appeler reset() depuis un test concurrent, car il affecte les autres règles du serveur partagé.
Une seule règle de panne s’applique à une transaction. Utilisez times lorsque plusieurs
tentatives consécutives doivent subir le même échec, ou enregistrez la règle suivante après avoir
observé la tentative précédente. InboxTap contrôle la livraison SMTP ; la persistance,
l’idempotence et la déduplication métier restent des assertions appartenant aux tests de
l’application.
Choisir la bonne méthode
- Utilisez
waitForLink()pour les URL de vérification, de réinitialisation de mot de passe et de lien magique. - Utilisez
waitForCode()pour les OTP numériques ; passezpatternpour un format non standard. - Utilisez
waitForMatch()pour une clé d’API ou une autre valeur intégrée dans le corps. - Utilisez
waitForMessage()quand l’assertion a besoin des en-têtes, des destinataires, du HTML ou de la source brute. - Utilisez
messages()quand l’email est peut-être déjà arrivé et que vous devez inspecter l’ensemble courant.
Toutes les méthodes d’attente sont bornées. Réglez timeoutMs assez haut pour le parcours applicatif,
mais gardez-le sous le délai d’expiration propre à l’outil d’exécution pour que les échecs remontent d’abord le contexte
InboxTap.