All posts
ci-cd github-actions tutorial

Email Testing in CI/CD: A Practical Guide

How to wire MailFork into your GitHub Actions pipeline for end-to-end email coverage on every pull request.

MailFork Team ·

Every pull request that touches an email flow should verify that flow end-to-end before it merges. Here’s how to wire MailFork into GitHub Actions to do exactly that — with isolated inboxes per run and automatic cleanup.

Prerequisites

Step 1 — Add your API key as a secret

In your GitHub repository: Settings → Secrets and variables → Actions → New repository secret

MAILFORK_API_KEY=mf_live_...

Step 2 — Install the SDK in your test environment

npm install @mailfork/sdk

Step 3 — Set up the MailFork client in your test suite

import { MailForkClient } from '@mailfork/sdk';

const mf = new MailForkClient({ apiKey: process.env.MAILFORK_API_KEY! });

Step 4 — Create a per-run alias

Aliases prevent test runs from seeing each other’s emails. Each alias is a unique address on your shared inbox; it expires automatically after the TTL.

let alias: Awaited<ReturnType<typeof mf.inboxes.createAlias>>;

beforeAll(async () => {
  alias = await mf.inboxes.createAlias({
    inbox: '[email protected]',
    ttlHours: 1,
    onExpire: 'delete',
  });
});

Step 5 — Write the email test

it('sends a password reset email and the link works', async () => {
  // trigger the flow using the alias address as the user's email
  await request(app)
    .post('/auth/forgot-password')
    .send({ email: alias.address })
    .expect(200);

  // wait for delivery (default timeout: 10s)
  const email = await mf.inboxes.waitForEmail({
    to: alias.address,
    subject: /reset your password/i,
    timeout: 10_000,
  });

  expect(email).toBeTruthy();

  // extract the reset link from the email body
  const match = email.body.html?.match(/href="(https:\/\/[^"]+\/reset-password[^"]*)"/);
  expect(match).toBeTruthy();

  const resetLink = new URL(match![1]);
  const token = resetLink.searchParams.get('token');

  // follow the link
  await request(app)
    .post('/auth/reset-password')
    .send({ token, password: 'new-password-123' })
    .expect(200);
});

Step 6 — Add the workflow step

# .github/workflows/ci.yml
name: CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    env:
      MAILFORK_API_KEY: ${{ secrets.MAILFORK_API_KEY }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm test

That’s it. Every run creates a fresh alias, uses it for the email flow, and cleans up after itself. No shared state between runs. No flaky tests from leftover emails.

Tips for robust email tests

Use waitForEmail with a timeout — don’t poll manually. The SDK handles backoff and timeout for you.

Match on subject, not just recipient — if your app sends multiple emails (welcome + verification), match on a subject pattern to pick the right one.

Check the plain-text body too — some email clients display plain text when HTML rendering fails. Assert that your plain-text version has the link.

Test attachment names and sizes — if your flow attaches a PDF receipt, verify email.attachments[0].filename and check it’s non-zero bytes.

Further reading

Try MailFork

Real email infrastructure for QA teams. Free tier includes 1 inbox and 500 emails/month.

Get started free