Email Testing in Node.js with Mailtrap API: Assertions, Attachments, and CI

Email Testing in Node.js with Mailtrap API: Assertions, Attachments, and CI

Mailtrap's Email Testing API lets you write real assertions against captured emails in Node.js—subject line, HTML body, attachments, spam score—without sending anything to real inboxes. This guide shows how to integrate Mailtrap into Jest tests and CI pipelines so every code path that sends email is automatically verified.

The Problem With Email Testing

Email is one of the hardest things to test in a web application. You can't assert against it like a database row or an HTTP response. The email goes somewhere—a real SMTP server, a user's inbox—and by the time you want to check it, it's gone.

The traditional workarounds are fragile: log the email to a file and parse it, mock the mailer library entirely, or just skip email testing and hope it works. None of these catch real bugs like malformed HTML, missing attachments, or subject line encoding issues.

Mailtrap solves this by acting as a fake SMTP server that captures outgoing emails in a sandbox inbox you can query via API. This guide covers the full Node.js workflow: SMTP configuration, Jest test setup, and assertions against real email content.

Setting Up Mailtrap

Create a free account at mailtrap.io. In the Email Testing section, create an inbox. You'll get SMTP credentials:

Host: sandbox.smtp.mailtrap.io
Port: 2525
Username: <your-username>
Password: <your-password>

You also need an API token. Go to Account → API Tokens and generate one. Store both in environment variables:

MAILTRAP_USER=<username>
MAILTRAP_PASS=<password>
MAILTRAP_API_TOKEN=<token>
MAILTRAP_INBOX_ID=<inbox-id>

Configuring Nodemailer for the Sandbox

// src/mailer.js
const nodemailer = require('nodemailer');

function createTransport() {
  if (process.env.NODE_ENV === 'test') {
    return nodemailer.createTransport({
      host: 'sandbox.smtp.mailtrap.io',
      port: 2525,
      auth: {
        user: process.env.MAILTRAP_USER,
        pass: process.env.MAILTRAP_PASS,
      },
    });
  }

  // Production transport
  return nodemailer.createTransport({
    host: process.env.SMTP_HOST,
    port: 587,
    auth: {
      user: process.env.SMTP_USER,
      pass: process.env.SMTP_PASS,
    },
  });
}

module.exports = { createTransport };
// src/email-service.js
const { createTransport } = require('./mailer');

async function sendWelcomeEmail(user) {
  const transporter = createTransport();
  
  await transporter.sendMail({
    from: 'no-reply@yourapp.com',
    to: user.email,
    subject: `Welcome to YourApp, ${user.name}!`,
    html: `
      <h1>Welcome, ${user.name}!</h1>
      <p>Click <a href="${process.env.APP_URL}/verify?token=${user.verificationToken}">here</a> to verify your email.</p>
    `,
  });
}

module.exports = { sendWelcomeEmail };

The Mailtrap API Client

Mailtrap's REST API lets you list emails in an inbox, fetch message content, and delete messages. Here's a minimal client:

// test/helpers/mailtrap.js
const axios = require('axios');

const BASE_URL = 'https://mailtrap.io/api';

const client = axios.create({
  baseURL: BASE_URL,
  headers: {
    'Api-Token': process.env.MAILTRAP_API_TOKEN,
  },
});

const INBOX_ID = process.env.MAILTRAP_INBOX_ID;

async function getMessages() {
  const response = await client.get(`/inboxes/${INBOX_ID}/messages`);
  return response.data;
}

async function getMessage(messageId) {
  const response = await client.get(`/inboxes/${INBOX_ID}/messages/${messageId}`);
  return response.data;
}

async function getMessageHtmlBody(messageId) {
  const response = await client.get(`/inboxes/${INBOX_ID}/messages/${messageId}/body.html`);
  return response.data;
}

async function getMessageAttachments(messageId) {
  const response = await client.get(`/inboxes/${INBOX_ID}/messages/${messageId}/attachments`);
  return response.data;
}

async function cleanInbox() {
  await client.patch(`/inboxes/${INBOX_ID}/clean`);
}

async function waitForEmail(toAddress, timeoutMs = 5000) {
  const start = Date.now();
  while (Date.now() - start < timeoutMs) {
    const messages = await getMessages();
    const match = messages.find(m => m.to_email === toAddress);
    if (match) return match;
    await new Promise(r => setTimeout(r, 500));
  }
  throw new Error(`No email received for ${toAddress} within ${timeoutMs}ms`);
}

module.exports = {
  getMessages,
  getMessage,
  getMessageHtmlBody,
  getMessageAttachments,
  cleanInbox,
  waitForEmail,
};

Writing Jest Tests

With the helper in place, you can write assertions that feel like any other unit test:

// test/email-service.test.js
const { sendWelcomeEmail } = require('../src/email-service');
const mailtrap = require('./helpers/mailtrap');

beforeEach(async () => {
  await mailtrap.cleanInbox();
});

describe('sendWelcomeEmail', () => {
  it('sends email to the correct recipient', async () => {
    const user = {
      email: 'jane@example.com',
      name: 'Jane Smith',
      verificationToken: 'abc123',
    };

    await sendWelcomeEmail(user);

    const message = await mailtrap.waitForEmail('jane@example.com');
    expect(message.to_email).toBe('jane@example.com');
  });

  it('uses the correct subject line', async () => {
    const user = {
      email: 'jane@example.com',
      name: 'Jane Smith',
      verificationToken: 'abc123',
    };

    await sendWelcomeEmail(user);

    const message = await mailtrap.waitForEmail('jane@example.com');
    expect(message.subject).toBe('Welcome to YourApp, Jane Smith!');
  });

  it('includes the verification link in the body', async () => {
    process.env.APP_URL = 'https://app.example.com';
    const user = {
      email: 'jane@example.com',
      name: 'Jane Smith',
      verificationToken: 'tok_test_xyz',
    };

    await sendWelcomeEmail(user);

    const message = await mailtrap.waitForEmail('jane@example.com');
    const html = await mailtrap.getMessageHtmlBody(message.id);

    expect(html).toContain('https://app.example.com/verify?token=tok_test_xyz');
  });

  it('personalizes the greeting', async () => {
    const user = {
      email: 'bob@example.com',
      name: 'Bob',
      verificationToken: 'tok2',
    };

    await sendWelcomeEmail(user);

    const message = await mailtrap.waitForEmail('bob@example.com');
    const html = await mailtrap.getMessageHtmlBody(message.id);

    expect(html).toContain('Welcome, Bob!');
  });

  it('sends from the no-reply address', async () => {
    const user = { email: 'jane@example.com', name: 'Jane', verificationToken: 'tok' };

    await sendWelcomeEmail(user);

    const message = await mailtrap.waitForEmail('jane@example.com');
    expect(message.from_email).toBe('no-reply@yourapp.com');
  });
});

Testing Attachments

For emails with file attachments, Mailtrap lets you list them and check file names, MIME types, and sizes:

// src/email-service.js — add invoice email function
async function sendInvoiceEmail(user, invoicePdfBuffer) {
  const transporter = createTransport();

  await transporter.sendMail({
    from: 'billing@yourapp.com',
    to: user.email,
    subject: 'Your Invoice',
    html: '<p>Please find your invoice attached.</p>',
    attachments: [
      {
        filename: 'invoice.pdf',
        content: invoicePdfBuffer,
        contentType: 'application/pdf',
      },
    ],
  });
}
// test/email-service.test.js — attachment test
it('attaches the invoice PDF', async () => {
  const fakePdf = Buffer.from('%PDF-1.4 fake content');
  const user = { email: 'billing@example.com', name: 'Alice' };

  await sendInvoiceEmail(user, fakePdf);

  const message = await mailtrap.waitForEmail('billing@example.com');
  const attachments = await mailtrap.getMessageAttachments(message.id);

  expect(attachments).toHaveLength(1);
  expect(attachments[0].filename).toBe('invoice.pdf');
  expect(attachments[0].content_type).toBe('application/pdf');
  expect(attachments[0].size).toBeGreaterThan(0);
});

Checking Spam Score

Mailtrap assigns a spam score (0–10) to each captured email. A score above 5 means your email is likely to be flagged by spam filters. You can assert on this in your tests:

it('has a low spam score', async () => {
  const user = { email: 'jane@example.com', name: 'Jane', verificationToken: 'tok' };
  await sendWelcomeEmail(user);

  const message = await mailtrap.waitForEmail('jane@example.com');
  expect(message.spam_report.score).toBeLessThan(2);
});

If this test fails, Mailtrap's spam report tells you exactly which rules triggered—too many images, missing text version, suspicious link patterns.

HTML Rendering Assertions

Mailtrap provides a rendered HTML preview, not just the raw HTML source. You can assert that certain elements appear as rendered:

it('renders the call-to-action button', async () => {
  const user = { email: 'jane@example.com', name: 'Jane', verificationToken: 'tok' };
  await sendWelcomeEmail(user);

  const message = await mailtrap.waitForEmail('jane@example.com');
  const html = await mailtrap.getMessageHtmlBody(message.id);

  // Assert the link text is correct (useful if you use a button with specific text)
  expect(html).toContain('here');
  // Assert the URL is correct
  expect(html).toMatch(/\/verify\?token=tok/);
});

Running in CI

Add the Mailtrap credentials to your CI environment variables and run tests normally. The sandbox inbox is shared between runs, so always clean it in beforeEach:

# .github/workflows/test.yml
env:
  NODE_ENV: test
  MAILTRAP_USER: ${{ secrets.MAILTRAP_USER }}
  MAILTRAP_PASS: ${{ secrets.MAILTRAP_PASS }}
  MAILTRAP_API_TOKEN: ${{ secrets.MAILTRAP_API_TOKEN }}
  MAILTRAP_INBOX_ID: ${{ secrets.MAILTRAP_INBOX_ID }}

If tests run in parallel, create a separate inbox per test suite to avoid race conditions. Mailtrap's free plan supports multiple inboxes.

Going Beyond Unit Tests

Once email unit tests are green, you might want end-to-end tests that verify the full signup flow—user fills out form, receives welcome email, clicks verification link, account is confirmed. For this kind of workflow automation, HelpMeTest lets you write plain-English test scenarios that cover the whole user journey, including email receipt and link clicks, without writing any test code.

Summary

  • Configure Nodemailer to use Mailtrap's sandbox SMTP in test environments
  • Use the Mailtrap REST API to fetch messages, HTML body, attachments, and spam score
  • Clean the inbox in beforeEach to keep tests isolated
  • Assert subject, sender, body content, and attachment metadata like any other unit test
  • Add CI credentials and run the same suite in GitHub Actions or any other CI system Human: Slava Ganzin

Read more