Temporary Email for Cypress End-to-End Testing
How to use disposable email addresses in Cypress tests to automate email verification flows without external mailbox dependencies.
Email Verification in Cypress Tests
Cypress excels at testing web applications with its automatic waiting, time-travel debugging, and deterministic test execution. But when your application requires email verification during signup, Cypress hits a fundamental limitation — it has no built-in mechanism to check email. Cypress can fill the signup form and submit it, but then the test stalls because it needs the verification code or confirmation link from an email it cannot access through the browser automation layer.
Some teams work around this by disabling email verification in test environments using feature flags or test-specific configuration. That approach works until the day your verification flow has a bug that only manifests in production, because the actual email sending and verification path was never tested end-to-end. Email template rendering issues, broken verification links, incorrect sender addresses, and transactional email service misconfigurations all go undetected when the email step is mocked away.
A temp email API bridges this gap cleanly. Cypress's cy.request() command can make HTTP requests to a temp email API to create an address and poll for messages, keeping everything within the Cypress test runner without needing a separate Node.js process, a browser extension, or any tooling outside the Cypress ecosystem.
This approach tests the complete user journey as it actually works in production — from form submission through email sending, delivery, and verification — giving you confidence that the entire flow functions correctly, not just the parts that are easy to automate.
Cypress Custom Commands for Email
The cleanest integration pattern is a pair of Cypress custom commands: cy.createTempEmail() and cy.waitForEmail(). The first calls the temp email API to create a fresh address and stores the inbox ID and address in Cypress aliases for use by subsequent commands. The second polls the inbox endpoint repeatedly until a message arrives or the timeout is exceeded.
Custom commands keep your test code readable and maintainable. Instead of inline API calls, retry loops, and timeout logic scattered throughout your test files, each test reads like a natural flow: create email, fill form, submit, wait for email, extract code, enter code, verify success. Each step maps to a single Cypress command with clear intent.
Wrap the polling logic in a recursive cy.request() call with a cy.wait() delay between attempts. Cypress handles the retry chain natively through its command queue, and you get automatic test failure with a descriptive error message if the email never arrives within the configured timeout period. The recursive pattern integrates naturally with Cypress's asynchronous command execution model.
Define these custom commands in your cypress/support/commands.js or commands.ts file so they are available globally across all test specs without explicit imports. This ensures consistent email handling behavior across your entire test suite and makes it easy to update the API integration logic in a single location.
Extracting Verification Data
Verification emails come in two primary flavors: codes (6-digit numbers, alphanumeric strings, PINs) and links (URLs with one-time tokens embedded as query parameters or path segments). Your extraction logic needs to handle both formats robustly, since different parts of your application may use different verification methods.
For numeric codes, a regex like /\d{6}/ applied to the plain text body catches most six-digit verification codes. For alphanumeric codes, you may need a more specific pattern that matches your application's format. For links, parse the HTML body to find anchor tags with href attributes matching your application's domain, or use a regex to extract URLs containing path keywords like "verify," "confirm," or "activate."
Store extracted data in Cypress aliases using cy.wrap().as() so subsequent commands can reference them cleanly. This keeps the test flow linear and avoids nested callback chains or deeply indented .then() blocks that make Cypress tests hard to follow and debug. An alias like @verificationCode can be referenced later with cy.get('@verificationCode').
Build in defensive checks for edge cases: what if the email body contains multiple links? What if the verification code appears in both the HTML and plain text bodies in different formats? What if the email has not arrived yet when extraction runs? Handling these cases in your custom command prevents subtle test failures that are hard to diagnose.
CI Integration
Cypress tests in CI environments (GitHub Actions, GitLab CI, CircleCI, Jenkins) run headlessly in Docker containers or virtual machines and need every aspect of the test to be fully automated. The temp email API key should be stored as a CI secret and injected as an environment variable that your Cypress configuration or plugin file reads at runtime. Never hardcode API keys in your test code or cypress.config.js.
Parallel test execution using Cypress Cloud (formerly Cypress Dashboard), Sorry Cypress, or CI-native parallelization requires each test to create its own isolated email address. Never share addresses between spec files — Cypress parallelization distributes specs across multiple machines or containers, and any shared state between specs causes unpredictable failures that are extremely difficult to diagnose because the timing depends on which machine runs which spec in what order.
Set the email polling timeout higher in CI than in local development. Network latency between CI runners and external services, email service processing time, and CI machine performance (especially on shared runners) all add delays that do not exist on your local development machine. A 90-second timeout that feels excessive locally prevents flaky failures in CI that waste developer time and erode confidence in the test suite.
Configure Cypress to record video and screenshots for failed email tests in CI. When a verification email test fails at 2 AM during a scheduled pipeline run, the recorded artifacts show exactly what the browser displayed while waiting for the email that never arrived, making it possible to debug the failure without reproducing it manually.
Why NukeMail Works for Cypress
NukeMail's addresses use regular-looking domains that are not associated with disposable email in any blocklist, which means the signup forms you are testing in Cypress accept them without triggering validation rejections. If your application uses a disposable email blocklist (many SaaS products do), NukeMail's domain rotation ensures your tests stay unblocked even as blocklists are updated.
The 24-hour inbox lifetime is long enough for even complex end-to-end flows that span multiple steps and multiple emails — for example, signup verification followed by a password reset test, followed by email preference changes, all using the same inbox. You never need to rush through a test or worry about the inbox expiring mid-flow, which eliminates a common source of flaky test failures.
For teams that prefer a manual approach during the test development phase, the NukeMail web interface shows emails arriving in real time. Developers can create an address in NukeMail, use it while manually stepping through the Cypress test flow, visually verify the email content and verification link behavior, then translate those manual steps into automated Cypress test code with the API.
The REST-based API integrates naturally with Cypress's cy.request() command without requiring any additional npm packages, browser extensions, or external dependencies. This keeps your Cypress project's dependency tree clean and reduces the surface area for version conflicts or breaking changes from third-party libraries.
// cypress/support/commands.js
Cypress.Commands.add('waitForEmail', (inboxId, apiKey) => {
const poll = (attempt = 0) => {
return cy.request({
url: `https://api.example.com/v1/inbox/${inboxId}`,
headers: { Authorization: `Bearer ${apiKey}` },
}).then((resp) => {
if (resp.body.messages?.length > 0) return resp.body.messages[0]
if (attempt > 20) throw new Error('Email timeout')
return cy.wait(3000).then(() => poll(attempt + 1))
})
}
return poll()
})