React Email10 min read

Testing React Email Templates: Unit Tests, Preview Tests, and Real Client Testing

A complete guide to testing React Email templates: unit tests for props validation, visual regression with Playwright, real client testing (Gmail, Outlook, Apple Mail), and CI/CD integration.

R

React Emails Pro

February 27, 2026

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.

Email templates are code. They should be tested like code. This guide covers three layers: unit tests (props validation), preview tests (visual regression), and real client tests (Gmail, Outlook, Apple Mail).

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)
If you're using React Email for transactional flows (password resets, invoices, verification), you must test. A broken password reset email isn't just a bug—it's a support incident.

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

emails/password-reset.test.tsx
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();
  });
});
Use @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.

emails/password-reset.tsx
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

  1. Generate HTML snapshots of your email templates with different props
  2. Save reference screenshots (baseline)
  3. On each test run, compare new screenshots to baseline
  4. 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

tests/email-previews.spec.ts
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");
  });
});
Run preview tests in CI to catch visual regressions before they reach production. Playwright generates a diff image when screenshots don't match.

Testing dark mode

Email clients handle dark mode differently. Test both light and dark variants.

tests/email-dark-mode.spec.ts
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:

  1. Always test: Gmail (web + mobile), Apple Mail, Outlook 365
  2. Occasionally test: Outlook 2016/2019 (if B2B audience), Yahoo Mail
  3. 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
Outlook 2016/2019 use Word's rendering engine, not a browser. If you use flexbox, grid, or modern CSS, test in Outlook.

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

tests/e2e/password-reset.spec.ts
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();
});
Use a test email service like MailHog (local) or Mailosaur (hosted) to capture sent emails in tests.

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

.github/workflows/email-tests.yml
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:

  1. Runs unit tests (props validation)
  2. Runs visual regression tests (Playwright screenshots)
  3. 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
Keep a spreadsheet of tested clients and date last verified. Re-test critical flows (password reset, billing) every 3-6 months or when you make layout changes.

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.

If you're using our templates, they've already been tested across Gmail, Apple Mail, Outlook, and mobile clients. You only need to test your custom props and content changes.

Production-ready templates for every flow

Pick from 9 template packs built with React Email. One-time purchase, lifetime updates, tested across every major email client.

Browse all templates