skip to content

Why does a Cypress alias created in a before() hook work only in the first test?

level: juniorimportance: must knowfreq 78%

answer

  1. Think about how long a name survives
  2. Which hook runs once, which runs repeatedly
  3. Cypress empties per-test state before every test
  4. The second test reports a missing alias
  5. Compare before() and beforeEach() registration

basics

~20 s

Cypress clears every alias before each test. Mocha's before() hook runs once per suite, so its alias survives into the first test only, and later tests fail with could not find a registered alias. Register it in beforeEach() instead.

solid answer

~40 s

Aliases live in Cypress's per-test state, and Cypress empties that state once before every test, unconditionally. Mocha's `before()` runs once per suite, just ahead of the first test, so the name it registers belongs to that test and nothing re-registers it afterwards. The second test then fails on `cy.get('@catalogue')` with `cy.get() could not find a registered alias for: @catalogue`, and the same disappearance hits `this.catalogue`, because Mocha clears the properties it copied onto the test context. The fix is almost always to move the `.as()` call into `beforeEach()` so each test registers its own alias. That per-test lifetime is deliberate: it stops one test from silently depending on a name an earlier test happened to leave behind, which is exactly the coupling that makes a suite order-dependent.

code

javascript · 19 lines
javascript
describe('library catalogue', () => {
  // Broken: before() runs once, and Cypress empties the alias registry
  // before every test, so only the first test can read @featured.
  // before(() => {
  //   cy.request('/api/catalogue/featured').its('body').as('featured')
  // })

  beforeEach(() => {
    cy.request('/api/catalogue/featured').its('body').as('featured')
  })

  it('shows the featured shelf', () => {
    cy.get('@featured').should('have.length.greaterThan', 0)
  })

  it('links every featured title', () => {
    cy.get('@featured').should('have.length.greaterThan', 0)
  })
})

go deeper

for a junior

Be ready to say how long a Cypress alias lives and to name the hook that registers one for every test. Interviewers use this as a quick check that you have actually run a spec with more than one test in it.

for a middle

Explain where the alias registry lives and what empties it, and describe both read paths dying together — cy.get('@name') and this.name go at the same moment.

for a senior

Show how you would triage this in a real suite: read the available-aliases list in the error, decide whether the name was never registered or registered in the wrong hook, and keep expensive setup out of every test without giving up isolation.

for a principal

Own the argument for why per-test alias lifetime is worth its cost. It forbids a test from inheriting a name from its neighbour, which is the coupling that turns a green suite red the moment someone reorders or shards it.

## What an alias holds An **alias** is a name Cypress hangs on the subject of the command it is chained to. You create one with the child command `.as(name)` — it must be chained, so `cy.as('featured')` is an error — and you read it back with an `@` prefix through `cy.get('@featured')`, or through `cy.wait('@featured')` when the alias names an intercepted route. Behind that name Cypress keeps two things: an entry in its own **alias registry**, which lives in the runner's per-test state, and a copy of the resolved subject on Mocha's test context, which is what makes `this.featured` readable inside a `function () {}` test. Both of those are scoped to a single test. That is the whole of this question. ## The reset, and what triggers it Cypress resets its per-test state exactly once before each test, and the alias registry is part of what gets emptied. Three things follow: - The reset is **unconditional**. It is not something a configuration option turns off, and it is not a side effect of clearing the page between tests — the registry starts empty for every test whatever else the runner does. - Both read paths go together. `cy.get('@featured')` loses the entry and `this.featured` loses the property, because Mocha cleans up the context properties it created for the previous test. - Nothing carries over from a previous spec file either; a new spec begins with an empty registry for the same reason. ## Why `before()` is the trap Mocha's `before()` hook runs **once per suite**, ahead of the first test in it. `beforeEach()` runs ahead of every test. Line those two facts up against the reset and the failure explains itself: 1. Cypress resets state for the first test. 2. `before()` runs. `.as('featured')` writes `featured` into the registry that belongs to that first test. 3. The first test runs and `cy.get('@featured')` resolves, so the spec looks correct. 4. Cypress resets state for the second test. The registry is empty again. 5. `before()` does not run a second time, so nothing re-registers the name, and the second test fails on its first read of it. The spec reads as if it should work because the hook is right there at the top of the file and it clearly ran — the Command Log even shows it. What the log does not show is that the name it produced belonged to one test. ## The failure you will see The second test fails with a message of this shape: ``` cy.get() could not find a registered alias for: `@featured`. You have not aliased anything yet. ``` When the test did register other aliases, the second line becomes `Available aliases are: ...` and lists them, which is usually the fastest way to see that the name you expected is simply absent. Cypress treats a name that was never registered as a mistake in the spec rather than something to wait out, so the failure is reported straight away instead of after `defaultCommandTimeout` expires. A fast alias failure and a slow `cy.get()` timeout are different problems, and the timing tells you which one you are holding. ## Where to put `.as()` instead | where `.as()` runs | which tests can read it | |---|---| | Mocha's `before()` | the first test in the suite only | | Mocha's `beforeEach()` | every test in the suite | | inside the `it()` body | that test, and only after the command has actually run | - The default fix is to move `.as()` into `beforeEach()`, so every test registers its own copy of the name. It costs whatever the aliased command costs, once per test. - If the command behind the alias is genuinely slow, keep the slow part in `before()`, hold its result in a variable declared in the `describe` scope, and re-register a cheap alias in `beforeEach()` with `cy.wrap(value).as('featured')`. - Nested suites behave the way you would hope: an alias registered by a parent suite's `beforeEach()` is readable from a test in a nested `describe`, because that hook runs for the nested test too. ## Why the lifetime is a feature It is easy to read the reset as an inconvenience. It is a guarantee. Because the registry starts empty, **no test can quietly depend on a name another test happened to leave behind**. A test that passes on its own passes in the suite; a single test can be run in open mode and behave exactly as it does in a full run; the file can be reordered or split across machines without a class of failure that is miserable to reproduce. Runners that let setup leak between tests eventually grow a suite that only passes in one order. Cypress removes the option, and the `before()` hook surprise is the price of that.

  • Does a Cypress alias registered inside one it() block survive into the next test?
    No. Anything `.as()` registers — in a hook or in the test body — is cleared when Cypress resets state before the next test. The registry starts empty every time, which is why a test that passes on its own can never be broken by an alias some other test left behind.
  • In Cypress, are there names you cannot pass to .as()?
    Yes. `.as()` rejects the reserved words `test`, `runnable`, `timeout`, `slow`, `skip` and `inspect`, because they collide with properties Mocha already puts on the test context. It also rejects an empty string, and a name that starts with `@` — you register `featured` and only add the `@` when reading it back.

An alias is a sticky note on the desk, not a label on the shelf. Cypress clears the desk before every test, so a note left by a one-time setup pass is gone by the second test.

saying these in an interview costs you the question

  • Says an alias lasts for the whole spec file
  • Thinks a config option can switch the alias reset off
  • Calls a missing alias a timing problem to wait out
  • Believes this.name survives after cy.get('@name') stops working