Email is one of those things that's genuinely hard to test. It goes out through an SMTP server, lands in a real inbox, and you have no programmatic way to check what arrived. So most teams either test it manually or just assert that sendEmail() was called and call it done.
Neither tells you if the subject line was right, the link was valid, or the HTML rendered correctly.
That's why I built email support into Mokapi. Your backend connects to Mokapi's SMTP server exactly as it would a real mail server. Mokapi captures the message. Your test fetches it over HTTP and asserts on the content. No real emails, no inbox polling, no external dependencies.
The Config
Unlike Kafka or HTTP, email doesn't have a standard specification format. So Mokapi uses a simple YAML config. This is all you need:
mail: '1.0'
info:
title: "Email Workflows"
servers:
smtp:
host: :2525
protocol: smtp
Point your backend at localhost:2525 and Mokapi starts capturing everything.
Want IMAP too, so you can preview emails in a real mail client during development? Add one line:
imap:
host: :1430
protocol: imap
The Playwright Test
The test drives a real signup form, then fetches the captured email from Mokapi's HTTP API and asserts on it.
test('Email verification after signup', async ({ page, request }) => {
const recipient = randomEmail();
// Drive the signup form
await page.goto('');
const form = page.getByRole('form', { name: 'Sign Up' });
await form.getByLabel('Email').fill(recipient);
await form.getByLabel('Password').fill('SuperSecure123!');
await form.getByRole('button', { name: 'Sign Up' }).click();
await expect(form.getByText('Check your inbox to verify your email')).toBeVisible();
// Fetch the email from Mokapi
const mails = await request.get(
`http://localhost:8080/api/services/mail/Email%20Workflows/mailboxes/${recipient}/messages?limit=1`
);
const mailList = await mails.json();
expect(mailList[0]).toEqual(expect.objectContaining({
subject: 'Verify your email address',
from: expect.arrayContaining([expect.objectContaining({ address: 'noreply@example.com' })]),
to: expect.arrayContaining([expect.objectContaining({ address: recipient })])
}));
// Check the body and verification link
const res = await request.get(
`http://localhost:8080/api/services/mail/messages/${mailList[0].messageId}`
);
const mail = await res.json();
expect(mail.body).toContain(`verify?email=${encodeURIComponent(recipient)}`);
});
Random email per test run keeps things isolated. Playwright's request object handles the Mokapi API calls directly, so the same test that drives the browser also inspects the email. No extra HTTP client needed.
The IMAP Bonus
Automated assertions catch broken content. But they don't catch a layout that looks terrible in Outlook. Because Mokapi supports IMAP, you can connect any real mail client to localhost:1430 during development and see exactly what your users will see. It's the visual check your test suite can't do.
Full Walkthrough
This is the short version. The complete guide on my site covers the full config, the Express backend, all the Playwright helpers, and how it all fits together.
Working repo: mokapi-email-workflow
Happy to answer questions in the comments.
Top comments (0)