Email Testing Guide: How to Test Emails in Development Without Sending Them

Email Testing Guide: How to Test Emails in Development Without Sending Them

Email testing is one of the most overlooked areas of application quality. Sending real emails during tests is slow, unreliable, and leaks data. The solution is a fake SMTP server that captures outgoing messages so you can assert on them without touching a real inbox.

The Core Problem

Applications send emails for dozens of flows: signup confirmation, password reset, order confirmation, invoice delivery. Testing these flows with real email providers introduces:

  • Latency — waiting for SMTP delivery
  • Flakiness — network, spam filters, quota limits
  • Side effects — real users receive test emails
  • Cost — transactional email providers charge per send

The standard solution: intercept SMTP traffic with a local fake SMTP server.

Local SMTP Interception

Point your application's SMTP configuration at localhost:1025 (or any port your fake SMTP server listens on). The application believes it sent an email; the fake server captures it.

# .env.test
SMTP_HOST=localhost
SMTP_PORT=1025
SMTP_USER=
SMTP_PASS=

Your application code stays unchanged. Only the SMTP endpoint differs between environments.

Testing Email Content

Once emails are captured, assert on:

  • To/From/Subject — routing correctness
  • HTML body — content, links, personalization
  • Plain text body — accessibility fallback
  • Attachments — presence and content
  • Headers — Reply-To, List-Unsubscribe, custom headers
// Example with Nodemailer + Mailpit API
const response = await fetch('http://localhost:8025/api/v1/messages')
const { messages } = await response.json()
const email = messages[0]

expect(email.To[0].Address).toBe('user@example.com')
expect(email.Subject).toBe('Welcome to MyApp')
expect(email.HTML).toContain('Confirm your account')
expect(email.HTML).toContain('https://myapp.com/confirm/')

HTML Email Rendering

Email HTML is notoriously fragile. Clients like Outlook use Word's rendering engine. Gmail strips certain CSS. Test rendering across clients with:

  • Email on Acid — automated client rendering screenshots
  • Litmus — rendering + accessibility checks
  • MJML — write responsive email markup that compiles to client-safe HTML

For functional tests, validate the HTML structure directly:

import { JSDOM } from 'jsdom'

const dom = new JSDOM(email.HTML)
const doc = dom.window.document

// CTA button present and correct
const cta = doc.querySelector('a[data-testid="confirm-cta"]')
expect(cta).toBeTruthy()
expect(cta.href).toMatch(/\/confirm\/[a-z0-9]+/)

// Unsubscribe link
const unsub = doc.querySelector('a[data-testid="unsubscribe"]')
expect(unsub).toBeTruthy()

Testing Email Sequences

Multi-step flows (onboarding drips, trial expiry reminders) need sequence testing:

test('sends 3-email onboarding sequence', async () => {
  await clearInbox()
  
  await signupUser({ email: 'test@example.com' })
  
  // Day 0: welcome
  const welcome = await waitForEmail({ to: 'test@example.com', subject: /Welcome/ })
  expect(welcome.HTML).toContain('Get started')
  
  // Trigger day-3 email via time travel or direct trigger
  await triggerSequenceEmail('onboarding-day-3', 'test@example.com')
  
  const day3 = await waitForEmail({ subject: /tips/ })
  expect(day3.HTML).toContain('pro tips')
})

Deliverability Testing

Beyond content, test that emails will actually reach inboxes:

  • SPF/DKIM/DMARC — validate DNS records with dns.lookup
  • Spam score — SpamAssassin integration in CI
  • Bounce handling — test your bounce webhook with fake bounce notifications
# Check SPF record
dig TXT yourdomain.com <span class="hljs-pipe">| grep spf

<span class="hljs-comment"># Check DKIM
dig TXT mail._domainkey.yourdomain.com

CI Integration

Add email tests to your CI pipeline:

# .github/workflows/test.yml
services:
  mailpit:
    image: axllent/mailpit
    ports:
      - 1025:1025
      - 8025:8025

steps:
  - name: Run email tests
    env:
      SMTP_HOST: localhost
      SMTP_PORT: 1025
    run: npm test -- --testPathPattern=email

Choosing a Tool

Tool Self-hosted UI API Docker
Mailpit
MailHog
Mailtrap Cloud
smtp4dev

For CI and local dev, Mailpit is the current best choice — actively maintained, fast, and has a clean REST API. MailHog is older but still widely used.

Summary

Test emails the same way you test API responses: intercept, capture, assert. Run a fake SMTP server in CI, point your app at it, and write assertions against the captured messages. Your email flows become as testable as any other part of your application.

Read more