Browser test automation

Isolating test identities in Selenium email tests

Give each Selenium run a unique receive-only email identity, assert the message, then tear down—so parallel suites stop stealing each other’s OTPs.

Multiple stamped envelopes sorted into separate inbox trays for test isolation

Isolating test identities in Selenium email tests means every browser session gets a unique mailbox (or unique plus-address) so parallel runs cannot consume each other’s verification codes. Assert on the message that belongs to this run, then tear down the user and discard the mailbox handle.

Mailby’s Quick Inbox helps with manual receive-only checks. Design CI so you are not pretending a browser-scraped temp inbox is a stable API—use documented developer tooling when you automate receive, or keep email assertions behind test doubles when the product under test allows.

Selenium context and boundaries

Selenium drives browsers. Email arrives out-of-band. The classic flake:

  1. Test A and Test B both sign up with qa@shared.example
  2. Both trigger OTPs
  3. Whichever poller wins steals the code
  4. The other times out and “email is broken”

Root cause: shared identity, not Selenium itself.

Implementation pattern

1. Mint a unique identity per test

qa+{suite}-{uuid}@your-test-domain.example

or allocate a fresh receive-only address from your test mail harness / developer API.

Store it on a context object:

String email = "qa+checkout-" + UUID.randomUUID() + "@example.test";
driver.findElement(By.id("email")).sendKeys(email);

2. Trigger the app action

Complete signup in the browser under test (system you own/authorize).

3. Wait with bounded polling

Poll the mail harness for to: email with a timeout (e.g., 60s), not an infinite loop. Prefer API fetch over UI scraping.

4. Assert precisely

  • Subject matches
  • Body contains code matching \b\d{6}\b (or your format)
  • Link host matches staging

5. Teardown

Delete the app user via admin API; invalidate the mailbox lease; quit the WebDriver.

Counterexample (failure)

A suite reused one Quick Inbox address pasted into a shared .env. Three parallel Selenium jobs requested codes; two failed with “invalid code.” Fix: unique identity per job, not a bigger sleep.

Step table

Test stepTimeout/retryAssertionTeardown
Open signupn/aForm visible
Submit unique email10s page loadRedirect to “check email”
Poll for message60s, 2s intervalExactly one new message to this address
Extract OTPn/aCode matches pattern
Submit OTP10sLand on onboardingDelete user; clear mailbox
Negative: wrong OTP5sError stateSame teardown

Worked example

Goal: Selenium signup on staging.

  1. CI job id build-8412 creates qa+8412-3f2c@staging-mail.example.
  2. Browser submits that address.
  3. Mail harness API returns message id m_99 for that recipient only.
  4. Test enters OTP, asserts dashboard.
  5. Job deletes user qa+8412-3f2c and drops messages for that address.

Parallel job build-8413 never sees m_99.

Manual exploratory alternative: open Quick Inbox, copy a session address into a single non-parallel Selenium debug run, watch the message, then discard. Label that path “manual QA,” not “CI source of truth.”

Companion app-layer guide: Laravel welcome email testing.

Timing and cleanup edge cases

  • Clock skew between app servers and mail store → allow slack in “received after timestamp T.”
  • Retries send two codes → assert on the newest message.
  • Browser crash mid-OTP → teardown must still run in finally.
  • Hard-coded waits (Thread.sleep(30000)) → hide races; replace with conditional waits.

When a permanent / durable test mailbox is safer

  • Seeded demos reviewed by humans for days
  • Providers that block disposable domains in staging
  • Flows needing reply from the same address (Mailby will not send)

Short answers

What causes identity collisions in Selenium email tests?
Shared inboxes across parallel jobs.

What should I do first?
Namespace emails with job id + UUID; assert recipient equality.

When is a permanent address safer?
Long-lived shared staging personas.

What evidence changes the recommendation?
You need API-level receive in CI—evaluate /developers rather than browser automation against a consumer inbox UI.

Sources, test date, limitations

  • Pattern date: 2026-09-24.
  • External: W3C WebDriver for automation model; keep tests authorized against systems you own.
  • No claim that Mailby scrapes well inside Selenium; prefer APIs for CI.
  • Receive-only: cannot test outbound reply threads from the temp address.

Design patterns that scale

Factory per test, not per class

Create the email inside the test method or a fixture with function scope. Class-scoped identities reintroduce collisions when tests run in parallel at method level.

Propagate identity through the stack

Browser → app DB → mail harness query must all key on the same string. Log the email in failure artifacts so humans can grep mail stores.

Prefer API mail harnesses in CI

Selenium should drive the product UI. Mail retrieval should be HTTP/API. Scraping a consumer web inbox from Selenium couples you to unrelated UI changes and rate limits.

When you need programmatic receive, use Mailby’s documented developers flows rather than inventing brittle scrapers against /inbox.

Contract tests for OTP format

If the product changes from 6-digit to 8-digit codes, isolation will not save you. Keep a single regex helper shared by Selenium and API tests.

Parallelism models

ModelIsolation needNotes
Sequential CIMediumStill unique emails avoid residue
Parallel methodsHighUUID required
Multi-browser matrixHighInclude browser name in local part
Shared stagingExtremeNamespace per engineer + job id

Flake taxonomy

  1. Identity collision — fixed by unique emails
  2. Polling too short — increase bounded timeout with metrics
  3. Double send — assert newest
  4. Wrong environment mailer — assert recipient domain
  5. Clock skew — compare with skew allowance

Teardown checklist

  • driver.quit() in finally
  • Delete app user via API (UI delete is slower and flaky)
  • Invalidate mail harness data for that recipient
  • Upload artifacts only on failure to control costs

Local developer loop

For a quick debug, a human can use Quick Inbox beside Selenium IDE-style runs. Document that path as manual. Do not copy manual steps into CI YAML without an API.

How this differs from the email-test-automation hub

Hub = landscape. This page = Selenium identity isolation with a collision counterexample and a step/timeout/assert/teardown table you can paste into a playbook.

Sample Java helper

public final class TestEmails {
  public static String unique(String suite) {
    return "qa+" + suite + "-" + UUID.randomUUID() + "@example.test";
  }
}

Pass unique("signup") into page objects. Never read from a shared static field mutated by tests.

Sample polling pseudocode

deadline = now + 60s
while now < deadline:
  msgs = mailApi.list(to=email, after=testStart)
  if msgs: return newest(msgs)
  sleep 2s
fail("timeout waiting for mail")

Cap retries. Emit metrics on timeout causes.

Coordination with backend flags

Feature flags that disable mail in some environments should fail fast with a clear assertion (“mail disabled in env”) rather than timing out at 60s. Read a health endpoint before the wait loop.

Full narrative: from flake to green

A team ran signup tests nightly. Failures clustered at 02:15 UTC when two jobs overlapped. Engineers lengthened sleeps to 90 seconds—failures became rarer but slower. Root cause was a shared QA_EMAIL secret. Replacing it with per-job UUIDs dropped timeouts to near zero and cut suite time because polls ended early on unique matches.

Moral: isolation beats sleep.

Data residency and PII

Even QA emails can look like PII in logs. Prefer clearly synthetic domains reserved for testing. Redact message bodies in CI artifacts. Do not paste real customer addresses into Selenium configs.

Grid and cloud browser services

When using Sauce Labs / BrowserStack style grids, unique emails still matter. Also unique passwords. Seed data collisions across OS/browser cells are common if the app enforces global unique email without cleanup.

Auth alternatives in test builds

Some teams inject a test-only endpoint that returns the latest OTP for a given email in non-production. That can be secure if locked down—but it is an app feature, not a Selenium trick. Document it. If unavailable, mail harness APIs remain the honest path.

Definition of done for an email Selenium test

  • Unique identity
  • Bounded wait
  • Assert content + recipient
  • Teardown user + mail
  • Artifact on failure
  • No shared mutable email field

Anti-patterns to delete from the repo

  • Thread.sleep(60000) before reading mail
  • Hard-coded testuser@gmail.com
  • Reading the latest message in a shared inbox without recipient filters
  • Leaving users behind without teardown
  • Scraping consumer inbox DOM in CI

Replace each with unique identity, API poll, assert, teardown. Your future selves will not miss the flakes.

CIProvider environment variables

Suggested pattern:

MAIL_TEST_DOMAIN=tests.example
CI_JOB_ID=$GITHUB_RUN_ID

Compose qa+${CI_JOB_ID}-${UUID}@${MAIL_TEST_DOMAIN}. Document required DNS for that test domain. Keep credentials for the mail harness API in the secret store, not in the Selenium repo wiki.

Parallelism lab exercise

Take a green sequential suite. Force parallelism: 4 in CI. If email tests fail, you have hidden coupling. Fix identities until green under parallel load, then keep parallel on. Sequential-only green is a false confidence signal for email flows.

Page object pattern

public class SignupPage {
  public void register(String email, String password) { /* ... */ }
}

Pass email from the test, never construct a shared default inside the page object. Page objects that “helpfully” supply DEFAULT_EMAIL recreate the collision class.

Mail assertion library

Centralize assertOtpDelivered(email, pattern, timeout). Individual tests should not each invent polling. One library fix improves the whole suite when ESP latency changes.

Isolation is an engineering property you can measure: collision rate under parallel load should trend toward zero. If it does not, hunt shared state before blaming Selenium or the ESP. Document the identity scheme in the QA README so new tests copy the safe pattern by default instead of inventing another shared inbox.

Reporting template for flakes

When an email Selenium test fails, the ticket should include: job id, email used, mail harness query, app user id, screenshots, and whether teardown ran. Without those fields, “email flaky” becomes a myth that blocks progress. Fill the template even when you already know the fix—patterns emerge across tickets.

Summary for leads

Mandate unique emails in the UI testing standard. Review PRs for shared inbox anti-patterns the same way you review for hardcoded credentials. Parallel-safe email tests are a team habit, not a one-off hotfix when the nightly goes red.

If two tests can ever share an inbox, they will—usually at the worst hour of CI. Design uniqueness first.

Unique per test. Assert the recipient. Tear down every time.

These closing lines exist to lock the operational habit into the document so skimming still leaves a usable rule.

Conclusion

Isolation is the difference between a flaky email suite and a trustworthy one. Unique identity → bounded poll → precise assert → teardown. Use Quick Inbox for manual checks; use developers when your automation needs a documented receive interface.

For message HTML pitfalls that break assertions on mobile clients, see inline images in mobile email clients.

Try it on Mailby

Open a receive-only disposable inbox when a short-lived address fits the job — session-bound, with timed purge.