Testowanie maili w testach automatycznych i CI – rejestracja i reset hasła
Ostatnia aktualizacja: 26:10:2026
Czego nie powie Ci atrapa wysyłki
Większość zestawów testów zastępuje wysyłkę maili atrapą i sprawdza, czy została wywołana z właściwym odbiorcą i szablonem. To dobry test jednostkowy i warto go mieć. Ale procesy zależne od maila – potwierdzenie rejestracji, reset hasła, logowanie linkiem, zaproszenia – psują się w sposób, którego atrapa nigdy nie zobaczy:
- link prowadzi do
localhostalbo starej domeny, bo adres bazowy jest na środowisku testowym ustawiony inaczej, - token w linku jest podwójnie zakodowany, ucięty albo wygasa, zanim mail dotrze,
- proces w tle, który wysyła maile, nie działa na wdrożonym środowisku,
- dane logowania SMTP albo klucz API na tym środowisku są błędne lub unieważnione,
- szablon renderuje się na komputerze programisty, ale wywraca na danych produkcyjnych (brak imienia, długa nazwa produktu),
- wiadomość wychodzi, ale zostaje odrzucona albo trafia do spamu, bo domena nadawcy nie jest uwierzytelniona.
Każdy z tych błędów dociera do użytkownika jako „nie dostałem maila”. Test end-to-end, który czeka na prawdziwą wiadomość i klika prawdziwy link, wyłapie je wszystkie – dlatego kluczowe procesy mailowe zasługują przynajmniej na jeden taki test.
Trzy poziomy testowania maili
| Poziom | Co odbiera pocztę | Do czego się nadaje | Czego nie sprawdzi |
|---|---|---|---|
| Test jednostkowy | atrapa obiektu wysyłającego | logika: kto dostaje jaki mail i z jakimi danymi | wszystkiego po send() |
| Test integracyjny | lokalny serwer SMTP do testów (np. Mailpit) | renderowanie, linki i nagłówki – offline i szybko | prawdziwego doręczenia, uwierzytelnienia, filtrów spamu |
| Test end-to-end | prawdziwa skrzynka w prawdziwej domenie | całej ścieżki, od której zależy użytkownik | niewiele – ale jest wolniejszy i wymaga danych dostępowych |
Korzystaj ze wszystkich trzech w odpowiednich proporcjach: dużo testów jednostkowych, testy integracyjne dla każdego szablonu i kilka testów end-to-end dla procesów, których awaria odcina użytkowników od konta.
Schemat: wywołaj, poczekaj, wyciągnij, sprawdź
- Zapisz czas, a potem wywołaj wysyłkę tak, jak zrobiłby to użytkownik – wypełnij formularz rejestracji, poproś o reset hasła.
- Poczekaj na wiadomość w skrzynce testowej, odpytując ją, aż wiadomość dotrze albo minie limit czasu.
- Wyciągnij to, czego użyłby użytkownik: link weryfikacyjny, jednorazowy kod.
- Wykonaj i sprawdź: otwórz link, wpisz kod i upewnij się, że aplikacja jest we właściwym stanie.
W AnyMail skrzynka testowa to zwykła skrzynka w Twojej domenie, a każde z oficjalnych SDK ma wywołanie waitForMessage stworzone właśnie do tego kroku. Oto test Playwrighta w TypeScripcie:
import { expect, test } from '@playwright/test';
import { Anymail } from '@anymail/sdk';
test.beforeAll(async () => {
await Anymail.connect({ apiKey: process.env.ANYMAIL_API_KEY! });
});
/** The first link in an email body that contains `path` - works for text and HTML bodies. */
function findLink(body: string, path: string): string {
const links = [...body.matchAll(/https?:\/\/[^\s"'<>]+/g)].map((match) => match[0].replaceAll('&', '&'));
const link = links.find((candidate) => candidate.includes(path));
if (!link) throw new Error(`No link containing ${path} in the email`);
return link;
}
test('a new account can verify its email address', async ({ page }, testInfo) => {
// One inbox per parallel worker, so two workers never read each other's mail.
const inbox = `e2e-${testInfo.parallelIndex}@example.com`;
const runId = crypto.randomUUID().slice(0, 8);
const startedAt = new Date(Date.now() - 10_000); // tolerate a few seconds of clock skew
await deleteTestUser(inbox); // your app's own helper: remove an account left over from an earlier run
await page.goto('/register');
await page.getByLabel('Name').fill(`E2E ${runId}`);
await page.getByLabel('Email').fill(inbox);
await page.getByLabel('Password').fill(`pw-${runId}-${Date.now()}`);
await page.getByRole('button', { name: 'Create account' }).click();
const mail = await Anymail.waitForMessage({
inbox,
subject: 'Verify your email',
contains: runId, // the greeting includes the name we registered with
since: startedAt,
timeoutMs: 60_000
});
await page.goto(findLink(mail.body, '/verify-email'));
await expect(page.getByText('Your email address is verified')).toBeVisible();
await mail.moveToTrash();
});Ten sam pomysł dla resetu hasła – w Pythonie, z pytestem i Playwrightem:
import re
from datetime import datetime, timedelta, timezone
import pytest
from anymail_sdk import Anymail
from playwright.sync_api import Page, expect
APP_URL = "https://staging.example.com"
INBOX = "e2e-reset@example.com"
RESET_LINK = re.compile(r"https://[^\s\"'<>]+/reset-password[^\s\"'<>]*")
@pytest.fixture(scope="session", autouse=True)
def anymail():
return Anymail.connect() # ANYMAIL_API_KEY comes from a CI secret
def test_password_reset_email(page: Page):
started_at = datetime.now(timezone.utc) - timedelta(seconds=10)
page.goto(f"{APP_URL}/forgot-password")
page.get_by_label("Email").fill(INBOX)
page.get_by_role("button", name="Send reset link").click()
mail = Anymail.wait_for_message(inbox=INBOX, subject="Reset your password", since=started_at, timeout=60)
match = RESET_LINK.search(mail.body)
assert match, f"no reset link in: {mail.body}"
page.goto(match.group().replace("&", "&")) # HTML bodies escape &
page.get_by_label("New password").fill("correct-horse-battery-staple")
page.get_by_role("button", name="Set new password").click()
expect(page.get_by_text("Your password has been changed")).to_be_visible()
mail.move_to_trash()I w Go – dla testu backendu, który wywołuje wysyłkę przez Twoje API zamiast przez przeglądarkę:
var resetLink = regexp.MustCompile(`https://[^\s"'<>]+/reset-password[^\s"'<>]*`)
func TestPasswordResetEmail(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
mail, err := anymailsdk.Connect(ctx, anymailsdk.Options{APIKey: os.Getenv("ANYMAIL_API_KEY")})
if err != nil {
t.Fatal(err)
}
const inbox = "e2e-reset@example.com"
startedAt := time.Now().Add(-10 * time.Second)
if err := triggerPasswordReset(inbox); err != nil { // your app, e.g. POST /forgot-password
t.Fatal(err)
}
msg, err := mail.WaitForMessage(ctx, anymailsdk.WaitOptions{
Inbox: inbox,
Subject: "Reset your password",
Since: startedAt,
Timeout: time.Minute,
})
if errors.Is(err, anymailsdk.ErrWaitTimeout) {
t.Fatal("no password reset email arrived within a minute")
}
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = msg.MoveToTrash(context.Background()) })
link := strings.ReplaceAll(resetLink.FindString(msg.Body), "&", "&") // HTML bodies escape &
if link == "" {
t.Fatalf("no reset link in: %s", msg.Body)
}
resp, err := http.Get(link)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("reset link returned %d", resp.StatusCode)
}
}Wszystkie trzy przykłady odczytują klucz API ze zmiennej środowiskowej, która w CI jest sekretem. Dostęp do API jest częścią planu Pro w AnyMail; dokumentacja API opisuje każde wywołanie, z którego korzystają SDK.
Jak poprawnie czekać
Odpytuj z limitem czasu, nie usypiaj
sleep(10), a potem „przeczytaj skrzynkę” to klasyczne źródło niestabilnych testów maili: w gorszy dzień za krótko, w lepszy – stracony czas. Zamiast tego odpytuj skrzynkę i kończ, gdy tylko wiadomość się pojawi. waitForMessage domyślnie sprawdza skrzynkę co dwie sekundy, zwalnia, gdy API ogranicza liczbę żądań, a po upływie limitu kończy się czytelnym błędem (WaitTimeoutError w TypeScripcie i Pythonie, ErrWaitTimeout w Go).
Nie przegap szybkiego maila
Domyślnie waitForMessage przyjmuje tylko wiadomości, które dotrą po jego uruchomieniu, więc nie złapie przez pomyłkę wczorajszego maila. To jednak tworzy wyścig: jeśli aplikacja jest szybka i mail dotrze, zanim zacznie się czekanie, zostanie pominięty. Przykłady unikają tego, zapisując czas przed wywołaniem wysyłki i przekazując go jako since. Odejmij kilka sekund, żeby uwzględnić różnicę między zegarem maszyny CI a zegarem serwera.
Jaki limit czasu wybrać
Poczta między dwiema skrzynkami u tego samego dostawcy dociera w kilka sekund; poczta przechodząca przez internet zwykle też, ale nie zawsze. Jedną z przyczyn kilkuminutowych opóźnień jest greylisting: serwer odbiorcy celowo odrzuca pierwszą wiadomość z nieznanej sieci nadawcy i przyjmuje ponowienie kilka minut później. AnyMail stosuje greylisting dla poczty przychodzącej z internetu – gdy aplikacja wysyła przez innego dostawcę do skrzynki testowej w AnyMail, pierwsza wiadomość z sieci tego dostawcy do tej skrzynki zostanie przyjęta dopiero przy ponowieniu, co najmniej pięć minut później. Potem ta para jest zapamiętana, a sieć, która przeszła kontrolę kilka razy, w ogóle ją pomija. Poczta, którą aplikacja wysyła przez samo AnyMail do skrzynki w AnyMail, jest doręczana wewnętrznie i nigdy nie podlega greylistingowi.
Sześćdziesiąt sekund oczekiwania to rozsądna wartość domyślna dla procesów, których poczta jest doręczana wewnętrznie albo ze znanej już sieci. Przy zupełnie nowej konfiguracji uruchom test raz z długim limitem, żeby przejść przez greylisting, zamiast na stałe zwiększać wszystkie limity. Własny limit czasu frameworka testowego ustaw wyraźnie powyżej czasu oczekiwania, żeby przekroczenie zgłaszało się jako „mail nie dotarł”, a nie jako ogólny błąd testu.
Dopasowanie właściwej wiadomości
Wspólna skrzynka testowa dostaje pocztę z każdego przebiegu, gałęzi i od każdego programisty. Filtruj tak dokładnie, jak się da:
- Temat i nadawca (
subject,from) zawężają wybór do właściwego rodzaju maila. - Coś unikalnego dla przebiegu (
contains) usuwa wszelkie wątpliwości. Test rejestracji podaje imię z losowym identyfikatorem przebiegu, a powitanie w mailu zawiera to imię. Jeśli maile nie zawierają niczego, co wpisuje użytkownik, dopasuj wiadomość predykatem (matchw TypeScripcie i Pythonie,Matchw Go) – na przykład po tokenie odczytanym z testowej bazy danych. - Czas rozpoczęcia (
since) wyklucza wszystko, co starsze.
Wyciąganie linków i kodów
Czytaj tę treść, którą czytają użytkownicy. Wiele aplikacji wysyła HTML z alternatywną wersją tekstową; AnyMail zachowuje część HTML, jeśli jest dostępna, więc przeszukuj pole body wiadomości zamiast pola z samym tekstem i w linkach wziętych z HTML zamieniaj & z powrotem na & – link z zakodowanym ampersandem traci drugi parametr. Wąski wzorzec ścieżki, której się spodziewasz, jest pewniejszy niż „pierwszy link w mailu”, którym często okazuje się logo.
Wzorce, które zwykle wystarczają
kod jednorazowy: \b\d{6}\b
link weryfikacyjny: https://[^\s"'<>]+/verify-email[^\s"'<>]*Izolacja między testami i przebiegami
AnyMail doręcza pocztę tylko do utworzonych przez Ciebie skrzynek i ich aliasów (do 10 na skrzynkę, dodawanych w panelu Aliases skrzynki), więc nie wygenerujesz nowego adresu dla każdego testu tak, jak pozwalają na to adresy z plusem czy skrzynka catch-all. Zaplanuj izolację wokół kilku stałych skrzynek testowych:
- Jedna skrzynka na równoległy proces. Utwórz
e2e-0@doe2e-3@dla czterech procesów i wybieraj skrzynkę po numerze procesu (w Playwright –testInfo.parallelIndex), żeby dwa testy nigdy nie czekały jednocześnie w tej samej skrzynce. - Przywracaj stan przed testem, nie po nim. Test rejestracji potrzebuje wolnego adresu, więc na początku usuń konto testowe pozostałe po wcześniejszym przebiegu. Sprzątanie na końcu zostaje pominięte właśnie wtedy, gdy test się wywróci.
- Porządkuj skrzynkę. Przeniesienie wiadomości do Kosza po teście sprawia, że skrzynka jest mała i szybko się ją przeszukuje; Kosz opróżnia się sam po upływie okresu przechowywania.
- Oddziel pocztę testową od prawdziwej. Klucz API ma dostęp do każdej skrzynki na koncie, do którego należy, więc klucz zapisany w CI nie powinien móc czytać Twoich produkcyjnych skrzynek. Jeśli możesz, umieść skrzynki testowe na osobnym koncie, a przynajmniej na osobnym serwerze pocztowym i w domenie testowej.
Niezawodność w CI
- Uruchamiaj testy end-to-end maili na środowisku, które wysyła prawdziwą pocztę – testowym albo produkcyjnym z kontami testowymi.
- Przy błędzie zapisz w logu, co się da: skrzynkę, przedział czasu, a jeśli wiadomość dotarła, ale nie pasowała – jej temat i Message-ID. Tak „niestabilny test” zamienia się w diagnozę.
- Pozwól najwyżej na jedno automatyczne ponowienie. Test, który przechodzi dopiero za trzecim razem, mówi Ci coś o procesie mailowym.
- Jeśli poczta dociera, ale u prawdziwych dostawców ląduje w spamie, skrzynka testowa tego nie pokaże – sprawdzaj doręczalność osobno, na podstawie nagłówków wiadomości wysłanej do Gmaila lub Outlook.com.
Lokalne serwery SMTP do testów, takie jak Mailpit
Do pracy lokalnej i testów integracyjnych lepszym narzędziem jest często lokalny serwer SMTP, który przechwytuje pocztę. Mailpit działa na przykład jako pojedynczy plik wykonywalny albo kontener Dockera (axllent/mailpit), przyjmuje pocztę przez SMTP na porcie 1025 i pokazuje ją w interfejsie WWW na porcie 8025. Wystarczy ustawić go jako serwer SMTP aplikacji, a nic nie opuści Twojego komputera. Ma REST API do testów integracyjnych, potrafi sprawdzić linki i zgodność HTML z programami pocztowymi, ocenić „spamowatość” wiadomości przez serwer SpamAssassin i ma tryb „chaos”, który odpowiada błędami SMTP, żebyś mógł sprawdzić, jak radzi sobie z nimi aplikacja.
| Lokalny serwer (Mailpit) | Prawdziwa skrzynka (np. AnyMail) | |
|---|---|---|
| Szybkość | natychmiast, działa offline | sekundy; czasem minuty przy pierwszym kontakcie |
| Konfiguracja | jeden kontener; zmiana serwera SMTP | domena, rekordy DNS i klucz API |
| Sprawdza produkcyjną ścieżkę poczty | nie – serwer testowy ją zastępuje | tak – ten sam dostawca, domena i uwierzytelnienie |
| Wyłapuje problemy z DNS i uwierzytelnieniem | nie | tak, jeśli poczta idzie prawdziwą ścieżką |
| Można uruchamiać przy każdym commicie | tak | tak, z umiarem – to prawdziwa poczta |
Pomiędzy tymi rozwiązaniami są usługi do testowania poczty w chmurze – warto się im przyjrzeć, jeśli potrzebujesz wielu jednorazowych adresów. Niezależnie od wyboru schemat pozostaje ten sam: wywołaj, poczekaj z limitem czasu, wyciągnij, sprawdź. A żeby strona wysyłająca była równie niezawodna, zajrzyj do poradnika o wysyłaniu maili z aplikacji.
Niech AnyMail zajmie się tymi rekordami za Ciebie
AnyMail generuje rekordy MX, SPF, DKIM i DMARC dla Twojej domeny, automatycznie sprawdza każdy z nich i wskazuje, który trzeba poprawić – a potem utrzymuje Twoje skrzynki. Usługa działa z Polski, dane przechowujemy w UE.