Skip to content
catchotp
Use case · E2E testing

E2E sign-up flows with real emails.

Stop mocking email in your end-to-end tests. Use a real inbox per test, real DKIM, real waiters — and zero cleanup.

  • Concurrent waiters
  • Ephemeral by default
  • Works in any CI
E2E testing pipeline: spec → ephemeral inbox → waitForOtp → all-green test run

The problem with mocking email

End-to-end tests exist to catch the integration bugs that unit tests miss. The moment you mock email, you stop catching the most common integration bugs in the first place: a missing DKIM record, a misconfigured From header, a transactional provider quietly throttling, a magic-link URL with the wrong host. The test passes; production does not.

The alternative — pointing the signup form at a shared QA mailbox — solves the integration problem and creates four new ones: parallel tests step on each other, the inbox accumulates years of junk, anyone can read anyone else's verification codes, and tearing down state between runs becomes its own ten-line cleanup script.

What you actually want is one inbox per test, on a real domain, with a waiter that resolves the moment the email lands. That is the entire shape of the catchotp API.

The three primitives you need

Per-test inboxes

One address per test, scoped by a Playwright or Cypress fixture. Auto-expire by TTL or delete explicitly in teardown.

Concurrent waiters

messages.waitFor() with subject regex or sender filter. Multiple tests block on different inboxes simultaneously without contention.

Structured message body

Every message returns parsed text, HTML, headers, deduped links, and detected OTPs. Pull the magic-link URL by hostname; never write a regex over raw HTML.

Mocked vs real email in E2E

Concern Mocked email Real email (catchotp)
DKIM / SPF coverage Not exercised Exercised end-to-end
Provider rate limits Hidden Surfaced
Parallel test isolation Manual One inbox per test, automatic
Test latency ~instant ~700ms p50
Cleanup burden Reset mock state TTL handles it

Playwright fixture + magic-link test

Define an inbox fixture once; reuse it across every test that needs email. Below: a Playwright fixture, a complete magic-link test, and the equivalent Cypress version.

// tests/fixtures/inbox.ts
import { test as base } from '@playwright/test';
import { CatchOTP, type Inbox } from '@catchotp/sdk';

export const test = base.extend<{ inbox: Inbox }>({
  inbox: async ({}, use) => {
    const otp = new CatchOTP({ apiKey: process.env.CATCHOTP_KEY! });
    const inbox = await otp.inboxes.create({ mode: 'ephemeral', ttlMinutes: 15 });
    await use(inbox);
    // No teardown needed — TTL handles it. Delete explicitly if you want it gone now.
    await otp.inboxes.delete(inbox.id).catch(() => {});
  },
});
export { expect } from '@playwright/test';
The fixture pattern keeps your tests focused on the behavior, not the plumbing.

E2E testing FAQ

Why not just use a Mailpit or MailHog container?
Self-hosted SMTP catchers are great for unit tests but they don't exercise the production path: real DNS, real MX, real TLS, real spam filters. Bugs in your DKIM/SPF setup or your transactional provider's rate limits never show up. catchotp uses a real internet-facing domain so the test traffic looks identical to production.
Can I run E2E tests in parallel?
Yes — every inbox is independent. The fixture pattern above gives each test its own address, so 50 concurrent tests get 50 isolated mailboxes. The free tier includes 5 inboxes; Pro gives you 50 concurrently.
What about cleanup between runs?
Ephemeral inboxes auto-expire (default TTL 60 minutes; configurable per inbox). You can also call inboxes.delete() in fixture teardown for instant cleanup. Either way you do not have to manage state across runs.
How do I extract a magic-link URL?
Every parsed message exposes a links array with the deduped URLs from the body. Filter by hostname or path to find the one you want. For OTP-style codes, use waitForOtp() instead of waitFor() to get the parsed code directly.
Does this work with Vercel / Netlify preview deploys?
Yes — point the test at the preview URL and use the same SDK key. The CI environment only needs outbound HTTPS access to api.catchotp.com.

Wire it into your suite this afternoon.

Free tier includes 5 inboxes and 1,000 messages a month — enough for most CI pipelines.