Skip to main content
Technology & EngineeringEmail Template347 lines

Email Testing

Email testing workflows using Litmus, Email on Acid, Mailtrap, and other QA tools

Quick Summary18 lines
You are an expert in email testing workflows and tools for ensuring email templates render correctly across clients and devices.

## Key Points

1. **Visual rendering**: Does the email look correct in each client?
2. **Responsiveness**: Does it adapt properly to mobile, tablet, and desktop?
3. **Dark mode**: Is the email readable in dark mode across clients?
4. **Accessibility**: Does it pass screen reader and contrast checks?
5. **Deliverability**: Will it land in the inbox or the spam folder?
6. **Functionality**: Do all links, buttons, and tracking pixels work?
7. **Data**: Do dynamic variables render correctly with different data sets?
- Test every template change in at least Apple Mail, Gmail (web), Outlook desktop, and one mobile client. These cover the vast majority of rendering engine variation.
- Use Mailtrap or a similar sandbox in development and CI to prevent accidental delivery to real recipients.
- Validate HTML size stays under 102KB. Gmail clips emails that exceed this threshold, hiding content behind a "View entire message" link.
- Automate structural validation (missing alt text, missing role attributes, unsafe CSS) as a pre-commit hook or CI step. Catch issues before visual testing.
- Maintain a test data fixture file with representative data for each template. Include edge cases: long names, missing optional fields, large item counts.
skilldb get email-template-skills/Email TestingFull skill: 347 lines
Paste into your CLAUDE.md or agent config

Email Testing — Email Templates

You are an expert in email testing workflows and tools for ensuring email templates render correctly across clients and devices.

Core Philosophy

Overview

Email rendering is notoriously inconsistent. A template that looks perfect in Apple Mail can break badly in Outlook or Gmail. Email testing involves validating HTML rendering across dozens of client/device combinations, checking spam score, verifying links, confirming accessibility, and testing dynamic content. Tools like Litmus, Email on Acid, and Mailtrap provide automated preview generation, spam analysis, and staging environments.

Core Concepts

Testing Dimensions

  1. Visual rendering: Does the email look correct in each client?
  2. Responsiveness: Does it adapt properly to mobile, tablet, and desktop?
  3. Dark mode: Is the email readable in dark mode across clients?
  4. Accessibility: Does it pass screen reader and contrast checks?
  5. Deliverability: Will it land in the inbox or the spam folder?
  6. Functionality: Do all links, buttons, and tracking pixels work?
  7. Data: Do dynamic variables render correctly with different data sets?

Priority Client Matrix

Focus testing effort on clients that represent your audience. A typical B2B priority list:

PriorityClientShare
CriticalApple Mail (iOS)~35-45%
CriticalGmail (web + app)~25-35%
CriticalOutlook (desktop + web)~10-15%
HighYahoo Mail~5%
MediumThunderbird~2%
MediumSamsung Mail~2%

Check your own ESP analytics for actual client distribution.

Implementation Patterns

Litmus Integration

Litmus provides email previews across 90+ client/device combinations and a design testing API:

// Litmus API — create an email test
const response = await fetch("https://api.litmus.com/v1/tests", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Basic ${Buffer.from(`${LITMUS_API_KEY}:`).toString("base64")}`,
  },
  body: JSON.stringify({
    test: {
      subject: "Order Confirmation Test",
      html_text: renderedHtml,
      clients: [
        "apple-mail-16",
        "gmail-web",
        "outlook-2021",
        "outlook-web",
        "iphone-15",
        "android-gmail",
      ],
    },
  }),
});

const test = await response.json();
console.log(`Litmus test created: ${test.url}`);

Mailtrap for Development and Staging

Mailtrap captures outgoing emails in a sandbox so they are never delivered to real recipients. It is ideal for development and CI environments:

import nodemailer from "nodemailer";

// Mailtrap SMTP configuration
const transporter = nodemailer.createTransport({
  host: "sandbox.smtp.mailtrap.io",
  port: 2525,
  auth: {
    user: process.env.MAILTRAP_USER,
    pass: process.env.MAILTRAP_PASS,
  },
});

// Send test email — captured by Mailtrap, never reaches real inbox
await transporter.sendMail({
  from: "noreply@example.com",
  to: "test@example.com",
  subject: "Welcome Test",
  html: renderedHtml,
  text: plainText,
});

Mailtrap also provides an API for checking captured emails in CI:

// Check inbox via Mailtrap API
const messages = await fetch(
  `https://mailtrap.io/api/v1/inboxes/${INBOX_ID}/messages`,
  {
    headers: { "Api-Token": process.env.MAILTRAP_API_TOKEN },
  }
).then((r) => r.json());

const latestMessage = messages[0];
console.log("Subject:", latestMessage.subject);
console.log("HTML analysis:", latestMessage.html_body_analysis);

Email on Acid Testing

Email on Acid provides rendering previews, accessibility checks, and spam testing:

// Email on Acid API — create a test
const response = await fetch("https://api.emailonacid.com/v5/email/tests", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Basic ${Buffer.from(`${EOA_API_KEY}:${EOA_API_PASSWORD}`).toString("base64")}`,
  },
  body: JSON.stringify({
    subject: "Test Campaign",
    html: renderedHtml,
    clients: [
      "iphone6s_14_dm",
      "gmail_web_current",
      "outlook_2019",
      "apple_mail_14",
    ],
  }),
});

const test = await response.json();
// Poll for results
const results = await fetch(
  `https://api.emailonacid.com/v5/email/tests/${test.id}/results`,
  { headers: { Authorization: `Basic ${credentials}` } }
).then((r) => r.json());

Automated HTML Validation

Run checks before visual testing to catch structural issues early:

import { validate } from "deep-email-validator";

// Validate email address format and MX records
const addressResult = await validate("recipient@example.com");
console.log(addressResult);

// HTML structure validation
function validateEmailHtml(html: string): string[] {
  const issues: string[] = [];

  // Check doctype
  if (!html.includes("<!DOCTYPE")) {
    issues.push("Missing DOCTYPE declaration");
  }

  // Check lang attribute
  if (!html.match(/<html[^>]*lang=/)) {
    issues.push("Missing lang attribute on <html>");
  }

  // Check for layout tables without role="presentation"
  const tableMatches = html.match(/<table(?![^>]*role="presentation")[^>]*>/g);
  if (tableMatches) {
    issues.push(`${tableMatches.length} table(s) missing role="presentation"`);
  }

  // Check images for alt text
  const imgWithoutAlt = html.match(/<img(?![^>]*alt=)[^>]*>/g);
  if (imgWithoutAlt) {
    issues.push(`${imgWithoutAlt.length} image(s) missing alt attribute`);
  }

  // Check for CSS properties that break in Outlook
  const unsafeCSS = [
    { pattern: /display\s*:\s*flex/g, name: "flexbox" },
    { pattern: /display\s*:\s*grid/g, name: "CSS Grid" },
    { pattern: /position\s*:\s*absolute/g, name: "absolute positioning" },
    { pattern: /float\s*:/g, name: "float" },
  ];
  for (const { pattern, name } of unsafeCSS) {
    if (pattern.test(html)) {
      issues.push(`Found ${name} — not supported in Outlook`);
    }
  }

  // Check total HTML size (recommended <102KB to avoid Gmail clipping)
  const sizeKB = Buffer.byteLength(html, "utf8") / 1024;
  if (sizeKB > 102) {
    issues.push(`HTML size is ${sizeKB.toFixed(1)}KB — Gmail clips at 102KB`);
  }

  return issues;
}

CI Pipeline Integration

# .github/workflows/email-tests.yml
name: Email Template Tests
on:
  pull_request:
    paths:
      - "emails/**"
      - "templates/**"

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install dependencies
        run: npm ci

      - name: Compile templates
        run: npm run build:emails

      - name: Run HTML validation
        run: npm run validate:emails

      - name: Check HTML size (Gmail 102KB limit)
        run: |
          for file in dist/emails/*.html; do
            size=$(wc -c < "$file")
            if [ "$size" -gt 104448 ]; then
              echo "FAIL: $file is $(($size / 1024))KB (exceeds 102KB Gmail clip limit)"
              exit 1
            fi
          done

      - name: Send to Mailtrap for visual QA
        run: npm run test:emails:mailtrap
        env:
          MAILTRAP_USER: ${{ secrets.MAILTRAP_USER }}
          MAILTRAP_PASS: ${{ secrets.MAILTRAP_PASS }}

      - name: Run Litmus visual tests
        if: github.event.pull_request.labels.*.name == 'email-change'
        run: npm run test:emails:litmus
        env:
          LITMUS_API_KEY: ${{ secrets.LITMUS_API_KEY }}

Link Validation

import { JSDOM } from "jsdom";

async function validateLinks(html: string): Promise<Array<{ href: string; status: string }>> {
  const dom = new JSDOM(html);
  const links = dom.window.document.querySelectorAll("a[href]");
  const results: Array<{ href: string; status: string }> = [];

  for (const link of links) {
    const href = link.getAttribute("href");
    if (!href || href.startsWith("mailto:") || href.startsWith("tel:")) {
      continue;
    }

    try {
      const response = await fetch(href, { method: "HEAD", redirect: "follow" });
      results.push({ href, status: response.ok ? "OK" : `HTTP ${response.status}` });
    } catch {
      results.push({ href, status: "UNREACHABLE" });
    }
  }

  return results;
}

Screenshot Comparison Testing

// Using Playwright to capture email rendering in a browser
import { test, expect } from "@playwright/test";
import fs from "fs";

test("email template visual regression", async ({ page }) => {
  const html = fs.readFileSync("dist/emails/welcome.html", "utf8");

  // Load email HTML directly
  await page.setContent(html);

  // Desktop viewport
  await page.setViewportSize({ width: 600, height: 800 });
  await expect(page).toHaveScreenshot("welcome-desktop.png", {
    maxDiffPixelRatio: 0.01,
  });

  // Mobile viewport
  await page.setViewportSize({ width: 375, height: 800 });
  await expect(page).toHaveScreenshot("welcome-mobile.png", {
    maxDiffPixelRatio: 0.01,
  });
});

Best Practices

  • Test every template change in at least Apple Mail, Gmail (web), Outlook desktop, and one mobile client. These cover the vast majority of rendering engine variation.
  • Use Mailtrap or a similar sandbox in development and CI to prevent accidental delivery to real recipients.
  • Validate HTML size stays under 102KB. Gmail clips emails that exceed this threshold, hiding content behind a "View entire message" link.
  • Automate structural validation (missing alt text, missing role attributes, unsafe CSS) as a pre-commit hook or CI step. Catch issues before visual testing.
  • Maintain a test data fixture file with representative data for each template. Include edge cases: long names, missing optional fields, large item counts.
  • Run link validation after rendering to catch broken URLs from bad template variables.
  • Keep a living document of known client-specific quirks your team has encountered, along with the workarounds used.
  • Use visual regression testing (Playwright screenshots) to catch unintended changes when refactoring templates.

Common Pitfalls

  • Testing only in a browser: Opening an HTML email in Chrome is not representative of how it will render in Outlook, Gmail, or mobile clients. Always use a cross-client testing tool.
  • Ignoring the Gmail 102KB clip: Large emails get truncated silently. If your footer, unsubscribe link, or tracking pixel is below the fold, it will be lost.
  • Not testing with real data: Templates that look fine with placeholder data can break with real-world inputs (long product names, many line items, missing optional fields).
  • Manual-only testing: Without automated CI checks, email regressions slip through during template refactors. Automate the checks that can be automated.
  • Testing dark mode in only one client: Apple Mail, Outlook app, and Gmail Android all handle dark mode differently. A template that looks correct in Apple Mail dark mode may be unreadable in Gmail Android.
  • Skipping plain-text testing: The plain-text version is often generated automatically and never reviewed. Check it manually to ensure it reads well.
  • Not testing image-blocked state: Add images being blocked to your test matrix. Verify alt text and styled alt text are acceptable.

Anti-Patterns

Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.

Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.

Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add email-template-skills

Get CLI access →