E-Mails in automatisierten Tests prüfen

Wie du Registrierung, Passwort-Reset und Bestätigungscodes in End-to-End-Tests mit echten Postfächern prüfst, ohne dass die Tests wackeln.

Aktualisiert am 20. Oktober 20265 Min. Lesezeit

Registrierung, Passwort-Reset, Einladungen, Zwei-Faktor-Codes: Viele wichtige Abläufe einer Anwendung führen über eine E-Mail. Trotzdem enden die meisten End-to-End-Tests genau dort, wo die Mail verschickt wird. Ob sie ankommt, ob der Link auf die richtige Umgebung zeigt und ob der Code im Text steht, prüft dann niemand. Dieser Ratgeber zeigt, wie du diese Lücke schließt, ohne dass deine Tests wackelig werden.

Drei Ebenen, drei Werkzeuge

Nicht jeder Test braucht ein echtes Postfach. Sinnvoll ist eine Aufteilung nach Ebenen:

Ebene Was du prüfst Typisches Werkzeug
Unit- und Feature-Tests Wird die richtige Mail mit den richtigen Daten ausgelöst? Mail-Fakes des Frameworks, etwa Mail::fake() in Laravel
Lokale Entwicklung Wie sieht die Mail aus, funktionieren die Links? ein lokaler SMTP-Fänger wie Mailpit
End-to-End in Staging und CI Kommt die Mail über den echten Versandweg an? ein echtes Postfach mit API

Die ersten beiden Ebenen sind schnell und sollten die Masse deiner Tests ausmachen. Ein echtes Postfach brauchst du für die wenigen Tests, die den ganzen Weg abdecken.

Wann ein echtes Postfach nötig ist

Mail-Fakes prüfen nur, dass deine Anwendung eine Mail erzeugen wollte. Was danach passiert, bleibt unsichtbar. Ein echtes Postfach findet Fehler wie diese:

  • Die Staging-Umgebung nutzt noch den Log-Mailer, und keine Mail verlässt den Server.
  • Der Bestätigungslink zeigt auf localhost oder auf die Produktion statt auf Staging.
  • Der Versanddienst lehnt Mails ab, weil die Absenderdomain nicht verifiziert ist.
  • Ein Template-Fehler zerstört den Code, sodass er im Text nicht mehr auftaucht.
  • Die Queue für den Mailversand läuft in dieser Umgebung gar nicht.

Das Muster: anlegen, auslösen, warten, auslesen, aufräumen

Fast jeder Mail-Test folgt denselben fünf Schritten:

  1. Anlegen: Der Test erzeugt eine frische, eindeutige Adresse.
  2. Auslösen: Er füllt das Formular deiner Anwendung mit dieser Adresse aus.
  3. Warten: Er wartet, bis die Mail ankommt, mit einer festen Obergrenze.
  4. Auslesen: Er holt den Code oder Link aus der Nachricht und nutzt ihn weiter.
  5. Aufräumen: Er löscht die Adresse wieder.

Mit der API von TossPost sieht das in Playwright so aus:

import { test, expect } from '@playwright/test';

const api = 'https://tosspost.com/api/v1';
const headers = {
    Authorization: `Bearer ${process.env.TOSSPOST_TOKEN}`,
    Accept: 'application/json',
};

test('Registrierung verschickt einen Bestätigungscode', async ({
    page,
    request,
}) => {
    const created = await request.post(`${api}/mailboxes`, {
        headers,
        data: { lifetime: 10 },
    });
    const { data: inbox } = await created.json();

    await page.goto('https://staging.example.com/registrieren');
    await page.getByLabel('E-Mail').fill(inbox.address);
    await page.getByRole('button', { name: 'Registrieren' }).click();

    const waited = await request.get(
        `${api}/mailboxes/${inbox.id}/messages/wait?timeout=30&since=${encodeURIComponent(inbox.created_at)}`,
        { headers, timeout: 35_000 },
    );
    expect(waited.status()).toBe(200);

    const { data: message } = await waited.json();
    await page.getByLabel('Code').fill(message.verification_code);

    await request.delete(`${api}/mailboxes/${inbox.id}`, { headers });
});

Warten statt schlafen

Der häufigste Grund für wackelige Mail-Tests ist ein festes sleep(5000). Mal reicht es nicht, mal verschenkt es Zeit. Besser ist eine Anfrage, die genau so lange offen bleibt, bis die Mail da ist.

Der Endpunkt messages/wait macht das: Er hält die Anfrage bis zu 30 Sekunden offen und antwortet mit 200 und der Nachricht, sobald sie eintrifft. Kommt in der Zeit nichts, antwortet er mit 204. Der Test schlägt dann mit einer klaren Meldung fehl, statt irgendwo später zu stolpern.

Achte auf den Parameter since. Ohne ihn zählt nur, was nach dem Start der Warte-Anfrage ankommt. Ist deine Anwendung schneller als der Test, wäre die Mail schon da und würde übersehen. Übergibst du als since den Zeitpunkt, an dem das Postfach angelegt wurde (created_at), kann dir keine Nachricht entgehen. Weil der Zeitpunkt vom Server stammt, spielen auch abweichende Uhren auf dem CI-Rechner keine Rolle.

Codes und Links auslesen

Jede Nachricht bringt die Felder verification_code und verification_url mit. TossPost erkennt Bestätigungscodes und Bestätigungslinks im Text bereits beim Empfang, du musst also keine eigenen regulären Ausdrücke pflegen. Brauchst du mehr, etwa um einen bestimmten Header oder den HTML-Aufbau zu prüfen, lädst du die vollständige Nachricht über messages/{id} oder das Original über messages/{id}/raw.

Parallel testen

Teile niemals eine Adresse zwischen Tests. Laufen zwei Tests gleichzeitig, liest sonst der eine den Code des anderen. Jeder Test legt sein eigenes Postfach an, das kostet nur eine Anfrage.

Wartende Anfragen sind pro Konto begrenzt, weil jede eine Verbindung offen hält. Bei vielen parallelen Tests antwortet die API mit 429, sobald alle Plätze belegt sind. Für große Testsuiten eignen sich deshalb Webhooks: TossPost ruft bei jeder neuen Nachricht eine Adresse deiner Wahl auf, unterschrieben mit einem HMAC-SHA256, den du vor dem Verarbeiten prüfst.

Tipps für stabile Mail-Tests

  • Token als Geheimnis speichern: Lege das API-Token in den Secrets deiner CI ab, nie im Repository.
  • Rechte begrenzen: Gib dem Token nur die Rechte, die der Test braucht, etwa Postfächer anlegen und Nachrichten lesen.
  • Kurze Lebensdauer wählen: Ein Postfach, das nach zehn Minuten verfällt, räumt sich selbst auf, falls der Test abbricht.
  • Klare Zeitlimits: Setze das Timeout des HTTP-Clients ein paar Sekunden über das Warte-Limit, damit die API zuerst antwortet.
  • Nur die wichtigsten Abläufe: Teste Registrierung, Passwort-Reset und Einladungen Ende zu Ende. Alles andere decken die schnelleren Ebenen ab.

Loslegen

Für die API brauchst du ein Konto bei TossPost. Unter Einstellungen → API erstellst du ein Token, und die API-Dokumentation beschreibt alle Endpunkte, Webhooks und die Prüfung der Signatur.

Zur API-Dokumentation – mit Beispielen für curl und Playwright.

Selbst ausprobieren