skip to content

Test Identities

Who receives the message under test: an address minted for one run versus a long-lived account a whole suite shares, and the residue either leaves. It decides whether a suite can run in parallel.

on this pageshow

questions

9

Why derive the recipient address in an automated sign-up test from the case and run identifiers?

level: juniorimportance: must knowfreq 52%

answer

  1. Who is this email actually for?
  2. Uniqueness is only half the benefit
  3. The address as a correlation key
  4. Case identifier plus run identifier, computed twice

basics

~20 s

A derived address is self-describing: built from the case and run identifiers it is unique, so concurrent runs cannot collide, and it names the case and run that sent the email, making a stray email diagnosable on sight.

solid answer

~50 s

Two properties matter here and only one of them is uniqueness. A random address per run keeps executions apart, but when an email arrives that nobody expected -- a duplicate, a late retry from the delivery path, a message caused by an earlier case -- a random string tells you nothing about where it came from. **Deriving** the address instead, from the case identifier plus the run identifier, turns the address into a correlation key: `signup-happy-path.r7c3@<domain-the-suite-owns>` says which case and which execution produced it. That buys three things beyond uniqueness: the assertion can be written as *the email at this address*, a failure names its own origin, and every address one run minted is selectable by prefix. Keep the derivation pure -- same inputs in, same address out -- so the case can compute it twice and never has to store it.

code

pseudocode · 12 lines
pseudocode
function recipientFor(caseId, runId):
    slug = shorten(kebab(caseId), 24)          # "signup-happy-path"
    return slug + "." + runId + "@" + SUITE_MAIL_DOMAIN

# --- arrange + act -------------------------------------------------
address = recipientFor(currentCase.id, run.id)
submitSignUpForm(emailField = address)

# --- assert: the same function, not a stored variable ---------------
address = recipientFor(currentCase.id, run.id)   # same inputs, same address
mail    = awaitOnlyMessageAt(address, deadlineSeconds = 60)
assert mail.body contains activationLinkPattern

go deeper

for a junior

Be ready to say why an automated sign-up test should not send to one fixed mailbox: each execution needs its own recipient so the email it asserts on is unmistakably the one it caused.

for a middle

Explain the derivation itself -- a stable case identifier plus one run identifier, combined the same way twice, short enough to pass the form's validation -- and why it is a pure function rather than a stored random value.

for a senior

Show what derivation buys in triage: an unexpected email names its own origin, one run's addresses are selectable together by prefix, and a rerun recomputes the same recipient with no shared storage between processes.

for a principal

Own the scheme as a convention several suites share: one derivation rule, one domain the organisation controls with catch-all routing, and a standing rule that no automated case ever sends to a real person's address.

## The address is the join key A case that exercises a notification flow -- a sign-up confirmation, a password reset, a receipt -- has to name a recipient before it can do anything else. Whatever address it names becomes the join between the action the case performed and the email that eventually arrives. That makes the address a **test-design decision** rather than a fixture detail: it is the only handle the case has on a message produced asynchronously, by a different process, seconds or minutes after the request returned. Three schemes turn up in real suites. A **fixed address** -- one mailbox the team owns -- is the default nobody chose. A **fresh random address per run** removes collisions. The third, and the one worth arguing for, is a **derived** address: a pure function of the case identity and the run identity, computed rather than stored. ## Deriving the address The rule is short: 1. Take a stable identifier for the case -- its name reduced to a short slug, or the numeric identifier the suite already assigns it. 2. Take one identifier for the run, minted once when the run starts and shared by every case in it. 3. Combine them deterministically into the local part, cap the length, and append a domain the test owns. 4. Mix in nothing that varies between two calls inside the same case: no timestamp read twice, no fresh random suffix, no attempt counter that the assertion does not also see. `signup-happy-path.r7c3@<domain-the-suite-owns>` is the whole idea. The case computes it when it fills the form and computes it again when it goes looking for the email; nothing is written down in between. ## What derivation buys beyond uniqueness Uniqueness is real, but it is the cheap half -- a random string delivers it. The rest of the value only shows up once something has gone wrong. | Property | Random per run | Derived from case + run | |---|---|---| | Two runs collide on one recipient | No | No | | An unexpected email names its own origin | No, it is an opaque string | Yes, the address says which case and run | | A rerun can find the same recipient | Only if the value was stored | Yes, recompute it | | One run's addresses can be selected together | No | Yes, by prefix | | Failure output is readable by a human | No | Yes | The middle rows are the ones that pay. When a run fails and the captured output shows an email nobody expected -- a duplicate send, a late retry from the delivery path, a message caused by a case that ran ten minutes earlier -- a derived address answers *whose is this?* on sight. An opaque random string forces you to look it up somewhere, and the somewhere is usually a process that has already exited. ## Reproducibility is the underrated half Because the address is a function, anything that knows the case and the run can regenerate it: the assertion, a retried attempt, a second process helping with triage, a script someone runs the next morning against the failure record. A stored random value has no such property -- it lives in the memory of the process that made it, and a crash takes it away. This also removes a class of coupling inside the case. Nothing has to carry the address from the setup step to the assertion through shared mutable state, and nothing needs a registry of *addresses this run created*. Two halves of the case agree on the recipient because they compute the same function, not because they passed a variable to each other. ## Practical constraints - **Length.** Registration forms and the product's own validation cap the local part. If a readable slug pushes the address past what the form accepts, hash the case identifier to a fixed-width code and keep the run identifier readable. - **Character set.** Lowercase letters, digits and one or two separators. Anything exotic tests the form's validator instead of the notification flow you meant to test. - **Routing.** The domain must accept every address the scheme can mint with no provisioning step -- a catch-all arrangement where any local part lands in one store. If each address has to be created first, the scheme costs a setup call per case and stops being free. - **Real people.** A derived address must resolve to a domain the suite controls. Deriving into a domain you do not own turns a failing test into unsolicited mail sent to a stranger. - **Do not over-derive.** Branch names, ticket numbers and personal data make the address churn and leak; the case identity and the run identity are enough. ## Where the scheme stops Deriving the recipient isolates the **mailbox**. It does not isolate the product's own records -- accounts, subscriptions, audit rows -- which need their own per-run namespacing, and that is a separate design question. It also assumes the product stores the address as given; a product that canonicalises addresses before keying an account can collapse several derived addresses into one. Both limits are worth knowing before you lean on the address as your only isolation boundary.

  • Why compute the address from the case rather than generate a random one and hold it in a variable?
    Because a function can be evaluated again and a variable cannot. A rerun, a second attempt, or someone triaging the failure the next morning can regenerate the exact recipient from the case and run identifiers alone. A random value lives only in the memory of the process that made it, so a crash takes the one handle on the message with it.
  • The derived address is too long and the sign-up form rejects it. What do you change?
    Shorten the case half, not the run half. Hash or truncate the case identifier to a fixed-width code so the local part fits the form's limit, keep the run identifier readable so failures still group by run, and put the full case name in the failure output rather than in the address. Never solve it by reverting to one shared recipient.
  • What must never be part of the derivation?
    Anything that differs between the two evaluations inside one case: a timestamp read at each call, a fresh random suffix, a counter the assertion does not also see. Also keep out personal data and anything that churns, such as branch names or ticket numbers. And the domain must be one the suite controls, or a failing test becomes unsolicited mail to a stranger.

It is the difference between an unmarked key and one with the room number stamped on it: both open exactly one door, but only one of them tells you which door when you find it on the floor.

saying these in an interview costs you the question

  • Says any unique random string is just as good
  • Builds a timestamp into the address, then cannot recompute it
  • Keeps the generated address only in memory, so triage cannot reconstruct it
  • Reuses one long-lived recipient and sorts the emails out afterwards
  • Derives into a domain the team does not control
  • Thinks a derived address removes the need to wait for delivery
open as a page

When many automated cases send email to one shared test mailbox, how does a case find the email it triggered?

level: juniorimportance: must knowfreq 42%

basics

~20 s

Generate a unique value inside the case and get it into the outgoing email — a subject-line tag or a custom header field — then search the shared mailbox for that exact value rather than for any recent message.

open as a page

How do leftover test identities produce both a false pass and a false failure in a messaging suite?

level: middleimportance: should knowfreq 36%

basics

~20 s

A leftover recipient holds an earlier run's copy of the notification, so an existence assertion goes green without the product sending anything. The same recipient may carry withdrawn consent or a suppression entry, so a correct product looks broken.

open as a page

A sign-up test resets the product's database between runs. What recipient state survives that reset?

level: middleimportance: should knowfreq 40%

basics

~20 s

Recipient identities live outside the product: the mailbox or number the run claimed, the address confirmed with the delivery side, the consent and subscription records, and any suppression entry. Resetting the product's tables removes none of them.

open as a page

How does a per-run recipient address stop two concurrent runs of the same password-reset test from interfering?

level: seniorimportance: should knowfreq 38%

basics

~20 s

A recipient minted for one execution makes the mailbox a partition of one: the only email there is the one this execution caused. Leftovers from earlier runs cannot satisfy the assertion, and concurrent runs cannot race for one single-use link.

open as a page

Two automated suites share one test recipient account and run at once — what breaks first?

level: seniorimportance: should knowfreq 32%

basics

~10 s

The account's single-writer state breaks before the mailbox does: an outstanding single-use code, a verification flag or a notification preference that can hold only one value. The second run silently overwrites the first run's.

open as a page

When a product strips address tags before keying an account, how does that break per-case recipient addresses?

level: middleimportance: nice to knowfreq 21%

basics

~20 s

Stripping the tag collapses every decorated variant into one canonical address, so identities the suite believes are separate become one account. Registration fails as a duplicate, or worse succeeds and two cases silently share state.

open as a page

Why does a case that asserts on the newest email in a shared test mailbox pass alone and fail in parallel?

level: middleimportance: nice to knowfreq 18%

basics

~20 s

Recency is not identity. In a shared mailbox the newest entry belongs to whichever case triggered a send most recently, so the assertion reads a neighbouring run's email — or a second email produced by your own action.

open as a page

Why do test recipient identities need a scheduled expiry sweep when per-case cleanup already runs reliably?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

Per-case cleanup only covers cases that reach their own end. A cancelled run, a process killed on a time limit, and a notification arriving after the case finished all leave identities behind. Some claims cannot be released on demand.

open as a page