Skip to content

Email Testing in CI/CD: Complete Guide

Most teams test their API endpoints, UI components, and database queries in CI/CD — but skip email testing. This guide shows you how to add email verification to your pipeline without the usual pain.

Email testing in pipelines has historically been painful:

  1. Infrastructure overhead — running MailHog or a fake SMTP server in every CI run
  2. Flaky timing — emails take variable time to arrive, leading to sleep(5000) hacks
  3. Shared state — multiple test runs reading from the same inbox cause race conditions
  4. No assertions — can’t programmatically check email content, subject, or OTP codes

Inboxical eliminates all of these problems:

  • No infrastructure — cloud API, no SMTP server to manage
  • Reliable timing — long-polling waits for emails instead of guessing
  • Isolated inboxes — each test creates its own inbox
  • Rich assertions — check subject, body, from, headers, and extract OTPs

Setting up email testing in GitHub Actions

Section titled “Setting up email testing in GitHub Actions”

Add INBOXICAL_API_KEY as a repository secret in GitHub Settings > Secrets.

name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps
- name: Run E2E tests
run: npx playwright test
env:
INBOXICAL_API_KEY: ${{ secrets.INBOXICAL_API_KEY }}
import { test, expect } from '@playwright/test'
import { inboxical } from '@inboxical/playwright'
test('password reset sends email', async ({ page }) => {
const inbox = await inboxical.createInbox()
await page.goto('/forgot-password')
await page.fill('[name=email]', inbox.emailAddress)
await page.click('[type=submit]')
const messages = await inboxical.waitForMessages(inbox.id)
expect(messages[0].subject).toContain('Reset your password')
// Extract the reset link from the email
const resetUrl = messages[0].body.match(/https:\/\/[^\s]+reset[^\s]+/)?.[0]
expect(resetUrl).toBeTruthy()
})
e2e-tests:
stage: test
image: mcr.microsoft.com/playwright:v1.40.0-jammy
script:
- npm ci
- npx playwright test
variables:
INBOXICAL_API_KEY: $INBOXICAL_API_KEY
jobs:
e2e:
docker:
- image: mcr.microsoft.com/playwright:v1.40.0-jammy
steps:
- checkout
- run: npm ci
- run:
command: npx playwright test
environment:
INBOXICAL_API_KEY: ${INBOXICAL_API_KEY}

If you’re not using Node.js, the REST API works from any language:

Terminal window
# Create an inbox
INBOX=$(curl -s -X POST https://api.inboxical.com/v1/inboxes \
-H "Authorization: Bearer $INBOXICAL_API_KEY" | jq -r '.id')
# ... trigger your email flow ...
# Wait for messages (long-polls for up to 30s)
MESSAGES=$(curl -s "https://api.inboxical.com/v1/inboxes/$INBOX/messages?wait=true" \
-H "Authorization: Bearer $INBOXICAL_API_KEY")
# Assert
echo $MESSAGES | jq -e '.[0].subject == "Welcome!"'
  1. One inbox per test — never share inboxes between tests
  2. Use long-polling — pass wait=true or use SDK methods instead of sleep()
  3. Clean up — delete inboxes after tests (or let them auto-expire)
  4. Test the critical paths — registration, password reset, OTP, and transactional emails
  5. Keep API keys secret — use CI/CD secret management, never commit keys
FlowWhat to assert
RegistrationWelcome email arrives, correct subject and sender
Password resetReset link is valid and unique
OTP/2FACode is correct length, works when entered
TransactionalOrder confirmation has correct details
NotificationsAlert emails trigger on the right events