Email Testing
Email testing workflows using Litmus, Email on Acid, Mailtrap, and other QA tools
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 linesEmail 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
- Visual rendering: Does the email look correct in each client?
- Responsiveness: Does it adapt properly to mobile, tablet, and desktop?
- Dark mode: Is the email readable in dark mode across clients?
- Accessibility: Does it pass screen reader and contrast checks?
- Deliverability: Will it land in the inbox or the spam folder?
- Functionality: Do all links, buttons, and tracking pixels work?
- 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:
| Priority | Client | Share |
|---|---|---|
| Critical | Apple Mail (iOS) | ~35-45% |
| Critical | Gmail (web + app) | ~25-35% |
| Critical | Outlook (desktop + web) | ~10-15% |
| High | Yahoo Mail | ~5% |
| Medium | Thunderbird | ~2% |
| Medium | Samsung 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
Related Skills
Dark Mode Email
Dark mode support patterns for email templates across major email clients
Email Accessibility
Accessible email design patterns for inclusive, standards-compliant email templates
Email Deliverability
Email deliverability essentials including SPF, DKIM, DMARC, and inbox placement
Mjml
Building responsive email templates with the MJML markup language and toolchain
React Email
Building email templates with React Email components and rendering pipeline
Responsive Email
Responsive email CSS patterns for consistent rendering across clients and devices