Most React Email templates ship untested. You write the component, check the dev preview, maybe send a test to your inbox, and call it done.
That works until you push a bug to production: broken links, missing props, layout shifts in Outlook, or subject lines that get cut off.
Why test email templates?
Unlike web pages, email rendering is unpredictable. A template that looks perfect in your browser can break in Gmail's mobile app or render blank in Outlook 2016.
Testing prevents:
- Production incidents (broken reset links, missing CTAs)
- Cross-client bugs (layout shifts, dark mode failures, image hiding)
- Data leaks (wrong user name, incorrect totals)
- Broken internationalization (missing translations, date formats)
Layer 1: Unit tests (props validation)
Unit tests verify that your email templates handle props correctly: required fields, conditional rendering, formatted values, and edge cases.
What to test
- Required props: Does the template fail gracefully if a required prop is missing?
- Conditional rendering: Are sections hidden/shown based on props?
- Formatted values: Are dates, currencies, and numbers formatted correctly?
- URLs: Do dynamic links resolve correctly?
- Fallbacks: Are defaults applied when optional props are missing?
Example: Password reset template
import { render } from "@react-email/render";
import { describe, it, expect } from "vitest";
import PasswordResetEmail from "./password-reset";
describe("PasswordResetEmail", () => {
it("renders reset link with correct token", () => {
const html = render(
<PasswordResetEmail
resetToken="abc123"
userName="Alice"
expiresIn="1 hour"
/>
);
expect(html).toContain("href=\"https://app.example.com/reset?token=abc123\"");
});
it("displays user name when provided", () => {
const html = render(
<PasswordResetEmail
resetToken="abc123"
userName="Alice"
/>
);
expect(html).toContain("Hi Alice");
});
it("falls back to generic greeting when userName is missing", () => {
const html = render(
<PasswordResetEmail resetToken="abc123" />
);
expect(html).toContain("Hi there");
});
it("shows expiration notice when expiresIn is provided", () => {
const html = render(
<PasswordResetEmail
resetToken="abc123"
expiresIn="30 minutes"
/>
);
expect(html).toContain("expires in 30 minutes");
});
it("throws error when resetToken is missing", () => {
expect(() => {
render(<PasswordResetEmail resetToken={undefined as any} />);
}).toThrow();
});
});@react-email/render to convert your email component to HTML string, then test the output with standard assertions.Type-safe props with Zod
Validate props at runtime with Zod schemas. This prevents sending emails with invalid data.
import { z } from "zod";
export const PasswordResetPropsSchema = z.object({
resetToken: z.string().min(1, "Reset token is required"),
userName: z.string().optional(),
expiresIn: z.string().optional(),
supportEmail: z.string().email().default("support@example.com"),
});
export type PasswordResetProps = z.infer<typeof PasswordResetPropsSchema>;
export default function PasswordResetEmail(props: PasswordResetProps) {
// Validate props at runtime
const validated = PasswordResetPropsSchema.parse(props);
return (
<Html>
<Head />
<Body>
<Text>Hi {validated.userName || "there"}</Text>
{/* ... */}
</Body>
</Html>
);
}Now your unit tests can verify Zod validation logic and ensure bad data never reaches production.
Layer 2: Preview tests (visual regression)
Preview tests catch visual regressions: layout shifts, style changes, broken spacing, and CSS bugs.
How preview testing works
- Generate HTML snapshots of your email templates with different props
- Save reference screenshots (baseline)
- On each test run, compare new screenshots to baseline
- Flag visual differences for review
Tools for preview testing
- Playwright: Headless browser testing with screenshot comparison
- Percy (optional): Visual regression as a service
- React Email CLI: Built-in preview server for quick visual checks
Example: Playwright visual test
import { test, expect } from "@playwright/test";
import { render } from "@react-email/render";
import PasswordResetEmail from "../emails/password-reset";
test.describe("Password Reset Email Previews", () => {
test("default state", async ({ page }) => {
const html = render(
<PasswordResetEmail
resetToken="abc123"
userName="Alice"
expiresIn="1 hour"
/>
);
await page.setContent(html);
await expect(page).toHaveScreenshot("password-reset-default.png");
});
test("no user name (fallback)", async ({ page }) => {
const html = render(
<PasswordResetEmail resetToken="abc123" />
);
await page.setContent(html);
await expect(page).toHaveScreenshot("password-reset-no-name.png");
});
test("long expiration message", async ({ page }) => {
const html = render(
<PasswordResetEmail
resetToken="abc123"
expiresIn="24 hours from now (or until you change your password)"
/>
);
await page.setContent(html);
await expect(page).toHaveScreenshot("password-reset-long-expiry.png");
});
});Testing dark mode
Email clients handle dark mode differently. Test both light and dark variants.
test("dark mode rendering", async ({ page }) => {
const html = render(<WelcomeEmail userName="Alice" />);
await page.setContent(html);
// Force dark mode via media query
await page.emulateMedia({ colorScheme: "dark" });
await expect(page).toHaveScreenshot("welcome-email-dark.png");
});Layer 3: Real client tests (Gmail, Outlook, Apple Mail)
Email clients ignore web standards. What looks perfect in Chrome might render blank in Outlook 2016 or break in Gmail's mobile app.
Testing strategy
You don't need to test every client on every change. Use this flow:
- Always test: Gmail (web + mobile), Apple Mail, Outlook 365
- Occasionally test: Outlook 2016/2019 (if B2B audience), Yahoo Mail
- One-time test: Proton Mail, Fastmail (for sensitive flows only)
Manual testing checklist
When sending test emails to real clients, verify:
- CTA buttons render correctly (not as text links)
- Layout doesn't break on narrow mobile screens
- Images load (or fallback text displays)
- Links are clickable and point to correct URLs
- Dark mode doesn't break contrast or hide content
- Text size is readable without zooming
- Spacing and alignment look correct
Automated client testing (optional)
Tools like Email on Acid or Litmus render your email in 90+ clients and generate screenshots automatically.
Worth it if:
- You have a B2B SaaS with corporate email users
- Your emails are revenue-critical (billing, invoices, dunning)
- You ship email changes frequently
Not worth it for early-stage products or simple transactional flows. Manual testing in Gmail + Apple Mail covers 80% of users.
Integration testing: End-to-end email flows
Test the full email-sending pipeline: trigger event → email queued → email sent → user receives email.
Example: Password reset flow
import { test, expect } from "@playwright/test";
test("password reset flow", async ({ page, context }) => {
// Step 1: Request password reset
await page.goto("https://app.example.com/forgot-password");
await page.fill('input[name="email"]', "test@example.com");
await page.click('button[type="submit"]');
// Step 2: Check for success message
await expect(page.locator("text=Check your email")).toBeVisible();
// Step 3: Fetch email from test inbox (using Mailhog, Mailosaur, etc.)
const email = await getLatestEmail("test@example.com");
expect(email.subject).toBe("Reset your password");
// Step 4: Extract reset link
const resetLink = extractResetLink(email.html);
expect(resetLink).toMatch(/\/reset\?token=[a-zA-Z0-9]+/);
// Step 5: Visit reset link and complete flow
await page.goto(resetLink);
await page.fill('input[name="password"]', "NewPassword123!");
await page.click('button[type="submit"]');
await expect(page.locator("text=Password updated")).toBeVisible();
});CI/CD integration: Automated testing on every PR
Run email template tests in your CI pipeline to catch bugs before they reach production.
Example: GitHub Actions workflow
name: Email Template Tests
on:
pull_request:
paths:
- 'emails/**'
- 'tests/email-*.spec.ts'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20
- run: npm ci
- run: npm run test:unit
- run: npx playwright install --with-deps
- run: npm run test:visual
- name: Upload visual diff artifacts
if: failure()
uses: actions/upload-artifact@v3
with:
name: visual-diff
path: tests/__screenshots__/diff/This workflow:
- Runs unit tests (props validation)
- Runs visual regression tests (Playwright screenshots)
- Uploads diff images if tests fail
Testing checklist: Ship with confidence
Before shipping a new email template to production:
- ✅ Unit tests: Props validation, conditional rendering, URL generation
- ✅ Preview tests: Visual regression in light and dark mode
- ✅ Manual client test: Send to Gmail (web + mobile), Apple Mail, Outlook
- ✅ Subject line: Check for truncation on mobile (50 chars max)
- ✅ Links: Click every CTA and verify correct destination
- ✅ Edge cases: Test with missing optional props, long text, special characters
- ✅ Accessibility: Alt text on images, semantic HTML, sufficient color contrast
Common testing mistakes
1) Only testing the happy path
Test edge cases: missing props, long text, special characters, unusual timezones, and unexpected user data.
2) Not testing dark mode
30%+ of users have dark mode enabled. Your email should look good (or at least readable) in both modes.
3) Skipping Outlook testing
If your audience includes corporate users, test in Outlook. It's the most unpredictable client and will break layouts that work everywhere else.
4) Testing only in the preview server
React Email's dev preview is useful, but it's not a real email client. Always send test emails to real inboxes.
5) Not validating props at runtime
TypeScript types disappear at runtime. Use Zod or similar to validate props when emails are sent (not just when they're authored).
Tools summary
Here's the testing stack recommended for React Email projects:
- Vitest: Fast unit tests for props validation
- Playwright: Visual regression and screenshot testing
- Zod: Runtime prop validation
- MailHog (dev) / Mailosaur (CI): Test email capture for E2E flows
- Litmus or Email on Acid (optional): Multi-client automated testing
Next steps
Start with unit tests. Add visual regression tests when you're making frequent layout changes. Only invest in automated multi-client testing if you have a B2B audience or revenue-critical email flows.
Most SaaS products can ship confidently with: unit tests + manual testing in Gmail and Apple Mail.