skip to content

In a Cypress spec, how do you give every test a value that a slow before() hook read once?

level: seniorimportance: should knowfreq 46%

answer

  1. Separate the expensive part from the named part
  2. Plain JavaScript scope outlives the alias registry
  3. A hook can rebuild a cheap alias each time
  4. cy.wrap costs nothing to re-run
  5. Shared values have to stay read-only

basics

~10 s

Keep the slow work in before() and capture its result into a variable in the describe scope from inside a .then() callback, then re-register a cheap alias for every test with cy.wrap(value).as('name') in beforeEach().

solid answer

~50 s

The alias registry is per test, so the alias cannot be hoisted — but the work behind it can. Run the slow command in `before()` and, inside its `.then()` callback, assign the resolved value to a `let` declared in the enclosing `describe`. That variable is ordinary JavaScript and lives as long as the spec file. Then add a `beforeEach()` that re-registers it with `cy.wrap(totalTitles).as('totalTitles')`. `cy.wrap()` is free, so every test gets a working alias and both read paths, while the request behind it ran once. Two cautions. The variable is undefined until the `before()` queue has drained, so read it only from code that runs later, never in the `describe` body. And the value is now shared by every test in the file, so treat it as read-only; if a test would mutate it, build it per test instead.

code

javascript · 22 lines
javascript
describe('library catalogue', () => {
  let totalTitles

  before(() => {
    cy.request('/api/catalogue/stats')
      .its('body.totalTitles')
      .then((count) => {
        totalTitles = count
      })
  })

  beforeEach(() => {
    cy.wrap(totalTitles).as('totalTitles')
  })

  it('shows the catalogue size on the landing page', () => {
    cy.visit('/catalogue')
    cy.get('@totalTitles').then((count) => {
      cy.get('[data-cy=catalogue-size]').should('have.text', String(count))
    })
  })
})

go deeper

for a junior

You mainly need the rule that a Cypress alias belongs to one test. Saying that much, and that beforeEach is where a repeated alias goes, is enough at this level.

for a middle

Explain the split: the slow command runs in before, its result lands in a JavaScript variable, and a cheap cy.wrap re-registers the alias each test. Be ready to say why the variable is not itself the alias.

for a senior

This is a shape you will actually build in a real suite. Show that you know what it costs — one value now shared by every test in the file — and how you keep that safe, rather than presenting it as free.

for a principal

Decide where the pattern is allowed at all. Sharing read-only reference data across a file is cheap; sharing anything a test can mutate buys minutes of runtime and pays for it in order-dependent failures nobody can reproduce locally.

## The constraint Cypress empties its alias registry before every test. No option extends an alias's life, and there is nowhere to register one that outlives the test it was made in. So the question is never "how do I keep the alias" — it is "how do I keep the **work**, and rebuild the alias cheaply". Separating those two is the whole technique: - The **expensive part** — a request, a fixture read, a task, a page visit — can run once, because its result is an ordinary JavaScript value once you get hold of it. - The **name** must be re-registered per test, because that is where the registry lives. Re-registering a value you already hold costs nothing. ## The shape 1. Declare a variable in the `describe` scope. It is plain JavaScript, so it lives as long as the spec file's module does. 2. In `before()`, run the slow command and assign its resolved value to that variable inside a `.then()` callback. The callback is where the value actually exists; the command itself yields into the queue, not into your code. 3. In `beforeEach()`, re-register the alias with `cy.wrap(value).as('name')`. `cy.wrap()` does no work beyond making the value a subject. 4. Read it in each test with `cy.get('@name')`, or as `this.name` in a `function () {}` test, exactly as you would have. ```javascript describe('library catalogue', () => { let totalTitles before(() => { cy.request('/api/catalogue/stats') .its('body.totalTitles') .then((count) => { totalTitles = count }) }) beforeEach(() => { cy.wrap(totalTitles).as('totalTitles') }) }) ``` ## The timing rule that catches people The `describe` body runs while the spec is being **defined**, before any command has executed. The variable is `undefined` at that moment, and anything reading it there reads nothing. - Reading it in the `describe` body, or at module top level, always sees `undefined`. - Reading it inside `beforeEach()` is safe, because `before()` has already run and its queue drained. - Reading it inside a test body or a `.then()` callback is safe for the same reason. - Writing `const totalTitles = cy.request(...)` stores a chainable object, not a number — the command has not run yet, which is why the assignment has to happen in the callback. ## What you bought, and what it cost | approach | setup cost | isolation | |---|---|---| | the whole `.as()` chain in `beforeEach()` | paid once per test | complete — nothing crosses a test | | slow work in `before()`, `cy.wrap().as()` in `beforeEach()` | paid once per file | the value is shared; the alias is not | You paid one request instead of six. You also created **one value shared by every test in the file**, which the per-test reset previously made impossible. That is a real trade: - Treat the shared value as **read-only**. `cy.wrap()` re-registers the same reference each test; it does not copy. If one test mutates a shared object, every later test sees the mutation. - Keep it **stable**. If something a test does can invalidate it — a record another test deletes, a token another test expires — it is not a candidate for sharing. - Check that every test still passes **on its own**. If one does not, the file has become order-dependent and the saving was not free. ## When to skip the alias entirely For a single value the variable alone is often clearer than the alias round-trip. The alias earns its keep when: - you want the value to appear in the Command Log under a readable name while debugging a failure; - the tests are written as `function () {}` and read `this.totalTitles` naturally; - helpers elsewhere in the suite already speak in `@` names and you want this value to fit that vocabulary. If none of those apply, using the variable directly in the assertion is a perfectly good line of code. ## Shapes to avoid Do not try to make the alias itself survive — by registering it twice in `before()`, by stashing it in a module-level object keyed by test name, or by reaching into a previous test's Mocha context. The registry is emptied wholesale and the context properties are cleaned up with it, so every workaround in that direction ends up rebuilding the per-test hook you were trying to avoid, with the isolation guarantee removed and nothing gained.

  • Why not use the describe-scoped variable directly and skip the alias?
    You can, and for a single value it is often clearer. The alias buys three things: the value appears in the Command Log under its name while you debug, it is readable as `this.totalTitles` in a `function () {}` test, and helpers that already speak in `@` names keep working. If none of those matter, the variable on its own is fine.
  • What breaks if the shared value is an object that one Cypress test mutates?
    Every later test sees the mutation, because there is one object and `cy.wrap()` re-registers the same reference rather than copying it. The file becomes order-dependent, and a developer running one test alone gets a different result from CI running the whole file. Freeze the object, hand each test a copy, or create the data per test.

saying these in an interview costs you the question

  • Tries to make the alias itself survive between tests
  • Reads the describe-scoped variable in the describe body
  • Claims the slow setup simply cannot be avoided
  • Shares a mutable object across tests without noticing
  • Thinks cy.wrap copies the value it re-aliases