コンテンツにスキップ

トランザクションメールをテストする

ユースケース: サインアップすると確認リンクが送られ、パスワードリセットはトークンを送り、注文すると領収書が届く。ユーザージャーニーがブラウザの外へ出ていく — テストはそれを受信箱まで追いかけ、また戻ってこなければならない。

Playwrightの守備範囲はブラウザまでです。メールはSMTP経由でアプリから出ていき、locator() で触れるものは何もありません。CIから本物のメールを送るのはさらに悪手です。遅く、レート制限があり、スパムフィルタに引っかかり、テストのトラフィックが実プロバイダに漏れます。

キャプチャ用SMTPサーバーを立て、アプリが送ったメールをREST APIで読み取ります。Mailpit(MailHogの後継)が現在の定番です:

docker-compose.test.yml
services:
mailpit:
image: axllent/mailpit
ports:
- '1025:1025' # SMTP — アプリの送信先をここに向ける
- '8025:8025' # Web UI + REST API

テスト対象アプリの送信先を smtp://localhost:1025 に向けたうえで:

import { test, expect } from '@playwright/test';
const MAILPIT = 'http://localhost:8025/api/v1';
async function waitForEmail(request, to: string, subject: RegExp) {
return expect
.poll(async () => {
const res = await request.get(`${MAILPIT}/search?query=to:${to}`);
const { messages } = await res.json();
return messages?.find((m) => subject.test(m.Subject)) ?? null;
}, { timeout: 15_000 })
.not.toBeNull()
.then(async () => {
const res = await request.get(`${MAILPIT}/search?query=to:${to}`);
const { messages } = await res.json();
return messages.find((m) => subject.test(m.Subject));
});
}
test('signup confirmation link works', async ({ page, request }) => {
const email = `user-${Date.now()}@example.test`;
await page.goto('/signup');
await page.getByLabel('Email').fill(email);
await page.getByRole('button', { name: 'Sign up' }).click();
// メッセージを見つけて…
const msg = await waitForEmail(request, email, /confirm your account/i);
// …HTML本文からリンクを抽出し…
const detail = await (await request.get(`${MAILPIT}/message/${msg.ID}`)).json();
const link = detail.HTML.match(/href="([^"]*\/confirm[^"]*)"/)?.[1];
expect(link).toBeTruthy();
// …ブラウザに戻ってジャーニーを完結させる。
await page.goto(link!);
await expect(page.getByText('Account confirmed')).toBeVisible();
});
  • 到達と宛先 — 正しい受信者、正しい件名。テストごとに一意のアドレス(user-${Date.now()}@…)を使えば実行同士が独立します。
  • リンクの往復 — メール内のトークンが実際に機能し、一度しか使えず、期限切れになること。
  • 本文の内容 — 名前や金額などのデータは部分一致でアサートし、本文全体のスナップショットは避けます。テンプレートは頻繁に変わります。
  • ネガティブケース — 配信停止が守られること、サインアップ失敗時にメールが送られないこと。少し待ってから GET /api/v1/messages を叩けば「何も送られていない」ことを証明できます。

MailpitはHTML本文のスクリーンショットも撮れるので、ビジュアルテストのフローに流し込めばテンプレートのリグレッションを検出できます。実クライアント(Outlookなど)でのレンダリング検証は専門サービス(Litmusなど)の領分で、CIでやることではありません。

  • テスト間でMailpitをリセットする(DELETE /api/v1/messages)か、すべてのクエリを受信者でスコープしてください。
  • アプリがコンテナ内で動いている場合、SMTPホストは localhost ではなく mailpit:1025 です。
  • 実プロバイダ(SES、SendGrid)経由で送信しているステージング環境も、送信先をMailpitのSMTPに差し替えられることが多いです。同じアサーションコードがデプロイ前にも後にも使えます。