E-Mail-Flows gehören zu den schwierigsten Teilen einer Webanwendung, die korrekt getestet werden müssen. Registrierungsbestätigungen, Passwort-Resets, Transaktions-E-Mails, Onboarding-Sequenzen — sie alle erfordern einen funktionierenden Posteingang zur Verifizierung.
Echte E-Mail-Adressen dafür zu verwenden ist eine schlechte Idee: Es verstopft Posteingänge, riskiert das Senden von Test-E-Mails an echte Nutzer und skaliert nicht für automatisierte Tests. Wegwerf- und temporäre E-Mail-Adressen lösen alle drei Probleme.
Dieser Leitfaden deckt das gesamte Werkzeugset ab — von schnellen manuellen Tests mit einem temporären Posteingang bis hin zur vollständig automatisierten E-Mail-Verifizierung in CI/CD-Pipelines.
Das Grundproblem beim E-Mail-Testing
Wenn du einen E-Mail-Flow aufbaust, musst du folgendes verifizieren:
- Die E-Mail wurde tatsächlich gesendet
- Sie wurde zugestellt (kein Bounce, kein Spam-Filter)
- Der Inhalt ist korrekt (Betreff, Inhalt, Links)
- Die Links in der E-Mail funktionieren korrekt (Bestätigungslinks, Passwort-Reset-Tokens)
- Der Flow schließt nach dem Klick auf den Link korrekt ab
Dies mit echten E-Mail-Adressen zu tun, schafft mehrere Probleme:
- Posteingangs-Verschmutzung — Die Posteingänge deines Teams füllen sich mit Hunderten von Test-E-Mails
- Versehentliche Sends an echte Nutzer — Eine falsch konfigurierte Umgebungsvariable und deine Test-E-Mails gehen an Produktionsnutzer
- Kein programmatischer Zugriff — Du kannst einen echten Posteingang nicht aus einer CI/CD-Pipeline heraus ohne OAuth oder IMAP-Konfiguration lesen
- Langsame Feedback-Schleifen — Einen echten Posteingang für jeden Testlauf manuell zu überprüfen, skaliert nicht
Tool-Kategorien für E-Mail-Tests
Es gibt vier Hauptansätze, jeder für unterschiedliche Situationen geeignet:
1. Öffentliche Wegwerf-Posteingänge (Manuelle Tests)
Dienste wie InstantTempEmail, Guerrilla Mail oder 10 Minute Mail geben dir sofort einen funktionierenden Posteingang. Verwende diese, wenn du einen Flow manuell testest und nur schnell eine Verifizierungs-E-Mail empfangen musst.
Wann verwenden:
- Lokale Entwicklungstests eines neuen E-Mail-Flows
- Einmalige QA-Prüfungen während der Entwicklung
- Testen, wie deine E-Mail in einem echten Posteingang dargestellt wird
Wann nicht verwenden:
- Automatisierte Tests — du kannst diese Posteingänge in einer Test-Suite nicht programmatisch lesen
2. Entwicklungs-SMTP-Catcher (Automatisiert, Isoliert)
Diese Tools fangen alle ausgehenden E-Mails in deiner Entwicklungs- oder Staging-Umgebung ab — nichts wird tatsächlich an echte Adressen zugestellt. Du inspizierst die erfassten E-Mails über eine Web-UI oder API.
Mailtrap ist am weitesten verbreitet. Du konfigurierst deine App, in Nicht-Produktionsumgebungen über deren SMTP-Server zu senden. Alle E-Mails gehen in einen Sandbox-Posteingang.
MailHog ist eine selbst gehostete Alternative — eine leichtgewichtige Go-Anwendung, die lokal einen SMTP-Server und eine Web-UI ausführt. Keine Kosten, keine externe Abhängigkeit.
Mailpit ist ein neuerer MailHog-Ersatz mit besserer UI und aktiver Entwicklung.
3. Öffentliche API-Posteingänge (Automatisiert, Mit Posteingangs-Zugriff)
Dienste wie Mailinator stellen öffentliche Posteingänge bereit, die über HTTP-API zugänglich sind. Du sendest eine E-Mail an testuser@mailinator.com von deiner App aus, dann überprüfst du den Posteingang über ihre API in deiner Test-Suite.
Wann verwenden:
- End-to-End-Tests, die E-Mail-Inhalte verifizieren und Links anklicken müssen
- CI/CD-Pipelines, wo du Posteingangs-Zugriff ohne OAuth benötigst
- Testen von e-mail-ausgelösten Flows (Bestätigung → Weiterleitung → Onboarding)
4. Transaktions-E-Mail-Testdienste
Dienste wie Mailtrap (ihr Testprodukt, getrennt von der Sandbox), Postmarks Testmodus oder SendGrids Sandbox-Modus testen die vollständige E-Mail-Versand-Pipeline — API-Aufrufe, Rendering, Zustellbarkeit — ohne tatsächlich zu senden.
MailHog für die lokale Entwicklung einrichten
MailHog ist der einfachste Weg, E-Mail-Tests lokal einzurichten. Es führt einen SMTP-Server aus, der alle ausgehenden E-Mails erfasst und in einer Web-UI anzeigt.
Installation mit Docker Compose
Füge MailHog zu deiner docker-compose.yml hinzu:
services:
mailhog:
image: mailhog/mailhog
ports:
- "1025:1025" # SMTP-Port
- "8025:8025" # Web-UI-Port
restart: unless-stopped
Deine App für die Verwendung von MailHog konfigurieren
In deiner .env für die Entwicklung:
MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=null
Alle E-Mails, die deine App in der Entwicklung sendet, gehen jetzt an MailHog. Greife auf den Posteingang unter http://localhost:8025 zu.
Warum das besser ist als echte E-Mail in der Entwicklung
- E-Mails verlassen niemals deine Maschine — keine versehentlichen Sends an echte Nutzer
- Sofortige Zustellung — kein Warten auf echte SMTP-Zustellung
- Vollständiger E-Mail-Inhalt im Browser sichtbar — Header, HTML, Textteile, Anhänge
- Keine Authentifizierung erforderlich
- Einfach zwischen Testläufen zu leeren
Automatisierte E-Mail-Tests mit der Mailinator-API
Für End-to-End-Tests, die E-Mails programmatisch lesen müssen, ist die Mailinator-API die unkomplizierteste Option.
Das kostenlose Kontingent hat Limits, aber ihre kostenpflichtige API ist für die meisten Teams erschwinglich.
Einfache Posteingangs-Prüfung mit fetch
async function getLatestEmail(inboxName) {
const response = await fetch(
`https://mailinator.com/api/v2/domains/mailinator.com/inboxes/${inboxName}`,
{
headers: {
Authorization: process.env.MAILINATOR_API_KEY
}
}
)
const data = await response.json()
if (!data.msgs || data.msgs.length === 0) {
throw new Error(`Keine E-Mails im Posteingang gefunden: ${inboxName}`)
}
return data.msgs[0] // Neueste E-Mail
}
async function getEmailBody(messageId) {
const response = await fetch(
`https://mailinator.com/api/v2/domains/mailinator.com/inboxes/test/messages/${messageId}`,
{
headers: {
Authorization: process.env.MAILINATOR_API_KEY
}
}
)
return response.json()
}
Vollständiger End-to-End-Registrierungstest mit Playwright
import { test, expect } from '@playwright/test'
const TEST_EMAIL = `e2e-test-${Date.now()}@mailinator.com`
test('Nutzerregistrierung und E-Mail-Bestätigung', async ({ page }) => {
// Schritt 1: Auf der Website registrieren
await page.goto('https://staging.deineapp.com/register')
await page.fill('[name="email"]', TEST_EMAIL)
await page.fill('[name="password"]', 'TestPassword123!')
await page.click('[type="submit"]')
// Erwarte Weiterleitung zur "Prüfe deine E-Mail"-Seite
await expect(page).toHaveURL(/check-email/)
// Schritt 2: Auf die Bestätigungs-E-Mail warten und sie abrufen
const email = await waitForEmail(TEST_EMAIL.split('@')[0], {
timeout: 30000,
subject: 'Bestätige deine E-Mail'
})
// Schritt 3: Bestätigungslink aus dem E-Mail-Body extrahieren
const confirmUrl = extractConfirmationLink(email.body)
expect(confirmUrl).toBeTruthy()
// Schritt 4: Den Bestätigungslink aufrufen
await page.goto(confirmUrl)
// Schritt 5: Erfolgreiche Bestätigung verifizieren
await expect(page).toHaveURL(/dashboard/)
await expect(page.locator('h1')).toContainText('Willkommen')
})
async function waitForEmail(inbox, options = {}) {
const { timeout = 15000, subject } = options
const startTime = Date.now()
while (Date.now() - startTime < timeout) {
const response = await fetch(
`https://mailinator.com/api/v2/domains/mailinator.com/inboxes/${inbox}`,
{ headers: { Authorization: process.env.MAILINATOR_API_KEY } }
)
const data = await response.json()
const emails = data.msgs || []
const match = subject
? emails.find(e => e.subject?.includes(subject))
: emails[0]
if (match) {
// Vollständigen E-Mail-Body abrufen
const bodyResponse = await fetch(
`https://mailinator.com/api/v2/domains/mailinator.com/inboxes/${inbox}/messages/${match.id}`,
{ headers: { Authorization: process.env.MAILINATOR_API_KEY } }
)
return bodyResponse.json()
}
await new Promise(resolve => setTimeout(resolve, 1000))
}
throw new Error(`E-Mail nicht innerhalb von ${timeout}ms empfangen`)
}
function extractConfirmationLink(emailBody) {
const htmlContent = emailBody.parts?.[0]?.body || ''
const urlPattern = /https?:\/\/[^\s"'<>]+confirm[^\s"'<>]*/i
const match = htmlContent.match(urlPattern)
return match?.[0] || null
}
Einzigartige Adressen pro Testlauf verwenden
Ein häufiger Fehler ist die Wiederverwendung derselben Test-E-Mail-Adresse über Testläufe hinweg. Alte E-Mails verschmutzen den Posteingang und Tests könnten die falsche E-Mail aufgreifen.
Muster: Timestamp-basierte eindeutige Adressen
// Einzigartigen Posteingang pro Testlauf generieren
const testRunId = Date.now()
const email = `registration-test-${testRunId}@mailinator.com`
Muster: UUID-basiert pro Test
import { randomUUID } from 'crypto'
function testEmail(prefix = 'test') {
return `${prefix}-${randomUUID().slice(0, 8)}@mailinator.com`
}
// Verwendung
const email = testEmail('registration') // registration-a1b2c3d4@mailinator.com
Dies stellt sicher, dass jeder Testlauf einen frischen Posteingang ohne Verlauf hat.
Mailpit einrichten (moderner MailHog-Ersatz)
Mailpit hat eine bessere UI, aktive Entwicklung und mehr Funktionen als MailHog. Es ist die bessere Wahl für neue Setups.
# docker-compose.yml
services:
mailpit:
image: axllent/mailpit
ports:
- "1025:1025" # SMTP
- "8025:8025" # Web-UI
environment:
MP_MAX_MESSAGES: 500
MP_DATABASE: /data/mailpit.db
MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1
volumes:
- mailpit_data:/data
volumes:
mailpit_data:
Mailpit hat auch eine REST-API für programmatischen Posteingangs-Zugriff, was es für automatisierte Tests in einer Docker-Compose-Umgebung geeignet macht:
// Mailpit-API — für die Verwendung in lokalen/CI-Docker-Compose-Setups
async function getMailpitInbox() {
const response = await fetch('http://localhost:8025/api/v1/messages')
return response.json()
}
async function deleteAllMessages() {
await fetch('http://localhost:8025/api/v1/messages', { method: 'DELETE' })
}
Leere den Posteingang zu Beginn jedes Testlaufs mit deleteAllMessages(), um einen sauberen Zustand zu gewährleisten.
Versehentliche Produktions-E-Mail-Sends verhindern
Das ist kritisch. Eine falsch konfigurierte Umgebungsvariable, die Staging auf Produktions-SMTP-Anmeldedaten zeigt, sendet echte E-Mails an echte Nutzer.
Ansatz 1: Umgebungsbasiertes SMTP-Switching
// email.config.js
const emailConfig = {
development: {
host: 'localhost',
port: 1025,
secure: false,
auth: null
},
test: {
host: 'localhost',
port: 1025,
secure: false,
auth: null
},
staging: {
host: process.env.MAILHOG_HOST || 'mailhog',
port: 1025,
secure: false
},
production: {
host: process.env.SMTP_HOST,
port: 587,
secure: true,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
}
}
export const smtpConfig = emailConfig[process.env.NODE_ENV] || emailConfig.development
Ansatz 2: Domain-Allowlist für Nicht-Produktionsumgebungen
function shouldSendEmail(toAddress) {
if (process.env.NODE_ENV === 'production') return true
const allowedTestDomains = [
'mailinator.com',
'guerrillamail.com',
'instanttempemail.com',
'deineunternehmen.com' // Interne Test-Adressen
]
const domain = toAddress.split('@')[1]
return allowedTestDomains.includes(domain)
}
E-Mail-Rendering über verschiedene Clients testen
Die oben genannten Tools verifizieren E-Mail-Zustellung und -Inhalt, aber nicht wie die E-Mail in verschiedenen Clients dargestellt wird. E-Mail-HTML verhält sich in Gmail, Outlook, Apple Mail und mobilen Clients unterschiedlich.
Für Rendering-Tests verwende Email on Acid oder Litmus — sie rendern Screenshots deiner E-Mail über 90+ Clients. Beide haben kostenlose Testversionen.
Für einfachere Prüfungen rendert die Mailtrap E-Mail-Vorschau-Funktion deine E-Mail in gängigen Client-Kontexten.
Häufig gestellte Fragen
Kann ich Gmail oder Outlook für automatisierte Tests verwenden? Technisch ja, mit OAuth oder IMAP-Zugriff. In der Praxis ist es langsamer, komplexer einzurichten und führt Rate-Limits und Authentifizierungsverwaltung ein. Dedizierte Tools sind immer schneller und zuverlässiger.
Was ist der Unterschied zwischen Mailtrap und MailHog? Mailtrap ist ein gehosteter SaaS-Dienst — deine E-Mails werden an ihre Server gesendet und sind in ihrer Web-UI einsehbar. MailHog ist selbst gehostet — es läuft auf deiner Maschine oder in deinem Docker-Compose-Setup. MailHog hat keine Kosten und keine Daten, die deine Umgebung verlassen. Mailtrap ist einfacher, im Team zu teilen.
Ist es sicher, Mailinator für sensible Testdaten zu verwenden? Nein. Mailinator-Posteingänge sind öffentlich — jeder, der den Posteingangs-Namen kennt, kann die E-Mails lesen. Verwende nur Testdaten. Verwende niemals echte Nutzerdaten in Mailinator-Posteingängen. Für private automatisierte Tests verwende stattdessen MailHog oder Mailpit in deinem Docker-Compose-Setup.
Wie teste ich E-Mail-Flows in einer GitHub-Actions-CI-Pipeline?
Füge einen MailHog- oder Mailpit-Dienst zu deinem GitHub-Actions-Workflow hinzu, indem du den services-Schlüssel in deinem Workflow-YAML verwendest. Deine App sendet an localhost:1025, und deine Tests lesen über die lokale API.
jobs:
test:
services:
mailpit:
image: axllent/mailpit
ports:
- 1025:1025
- 8025:8025
Sollte ich in Staging mit echten E-Mail-Anbietern testen? Nur wenn du speziell die Zustellbarkeit testen musst (Spam-Scoring, DKIM/DMARC, Posteingangs-Platzierung). Für funktionale Tests von E-Mail-Flows ist ein lokaler Catcher wie MailHog schneller und sicherer. Führe echte Zustellbarkeits-Checks separat, weniger häufig, mit Test-Accounts bei großen Anbietern durch.