skip to content

In Cypress, how do cy.get('@books') and this.books differ when reading an alias?

level: middleimportance: should knowfreq 60%

answer

  1. Two ways in, two different behaviours
  2. One replays work, one froze a value
  3. Arrow functions have no Mocha context
  4. Cypress stores a chain, not an element
  5. The queue must drain before this.name exists

basics

~20 s

cy.get('@books') re-runs the query chain stored with the alias, so it yields a fresh result on every read. this.books is a snapshot written onto Mocha's test context when .as() ran, and it needs a function() test, not an arrow.

solid answer

~50 s

`.as()` stores two things. It records the subject chain that produced the subject — the queries leading up to it — in Cypress's per-test alias registry, and it also copies the resolved subject onto Mocha's test context as `this.books`. Reading `cy.get('@books')` replays that stored chain against the page as it is now, so a DOM alias re-queries and a value alias re-derives; reading `this.books` hands back the value captured at the moment `.as()` ran and never re-evaluates. Two practical consequences follow. `this` works only in a test or hook written as `function () {}`, because an arrow function has no Mocha context, and `this.books` is undefined until the queued `.as()` command has actually run, so you cannot read it on the line after you write it. Both disappear when Cypress resets state before the next test.

code

javascript · 16 lines
javascript
const catalogue = { shelf: 'new-arrivals' }

it('reads one alias two ways', function () {
  cy.wrap(catalogue).its('shelf').as('shelf')

  cy.wrap(null).then(() => {
    catalogue.shelf = 'reserved'
  })

  cy.get('@shelf').should('equal', 'reserved')

  cy.wrap(null).then(function () {
    // Chai's expect, reading Mocha's test context
    expect(this.shelf).to.equal('new-arrivals')
  })
})

go deeper

for a junior

Know both ways to read a Cypress alias — cy.get('@name') and this.name — and remember that this.name needs a test written with function () {}, never an arrow function.

for a middle

Explain what .as() actually stores: a replayable subject chain in Cypress's registry, plus a resolved snapshot on Mocha's context. Be ready to predict which one changes after the underlying value mutates.

for a senior

Expect to be asked which style you enforce in a real suite and why, including how each behaves when the page re-renders mid-test and how each reads in a failure you are triaging under pressure.

for a principal

The interesting angle is consistency. Two read paths with different freshness semantics in one codebase is a standing source of confusion, so own the convention and the narrow exception that justifies the other one.

## Two names for one alias When `.as('books')` runs, Cypress does two separate things with the subject it was handed. 1. It records the **subject chain** — the sequence of queries that produced that subject — in its per-test alias registry, under the name `books`. 2. It assigns the resolved subject onto Mocha's test context, so the property `this.books` exists for the rest of that test. Two stores, read by two syntaxes, and they do not behave the same way. ## `cy.get('@books')` replays `cy.get()` recognises the `@` prefix, looks the name up in the registry, and runs the stored chain again against the page as it is now. For a DOM alias that means the selector chain is re-executed, so `cy.get('@rows').first().click()` acts on the row that matches at that moment rather than on a jQuery object captured earlier. For a value alias built from something like `cy.wrap(catalogue).its('shelf')`, the chain is re-derived from the same source object, so a mutation to that object shows up on the next read. Because it is an ordinary Cypress command, this path also behaves like one: it is enqueued rather than executed inline, it yields a subject to whatever you chain onto it, and assertions chained after it retry in the usual way. ## `this.books` is a snapshot The context property is written once, at the moment `.as()` executed, and is never updated. Read it later in the same test and you get exactly what was resolved then. Three consequences catch people: - **It needs `function () {}`.** An arrow-function test or hook has no Mocha context of its own — `this` is inherited lexically from the enclosing file — so `this.books` is undefined no matter what the hook did. That is why every Cypress example using `this` is written with the `function` keyword. - **It is not available synchronously after `.as()`.** The test body only builds the command queue; Cypress drains it after the body returns. Writing `cy.fixture('books.json').as('books')` and then reading `this.books` on the next line reads a property that has not been written yet. - **It is typed loosely.** In TypeScript the Mocha context is not narrowed for you, so `this.books` arrives untyped unless you declare it, whereas `cy.get('@books')` can be typed at the call site. ## Side by side | | `cy.get('@books')` | `this.books` | |---|---|---| | what is stored | the subject chain | the resolved subject | | freshness | re-derived on every read | frozen at `.as()` time | | syntax requirement | none | a test or hook written as `function () {}` | | available from | when the queued read runs | after `.as()` has run | | yields into | the command chain | a plain JavaScript value | ## The case that shows the difference Alias a property of a mutable object, then change the object: ```javascript const catalogue = { shelf: 'new-arrivals' } cy.wrap(catalogue).its('shelf').as('shelf') cy.wrap(null).then(() => { catalogue.shelf = 'reserved' }) cy.get('@shelf').should('equal', 'reserved') cy.wrap(null).then(function () { // Chai's expect, and Mocha's test context expect(this.shelf).to.equal('new-arrivals') }) ``` Both reads are correct. `cy.get('@shelf')` re-runs `cy.wrap(catalogue).its('shelf')` and finds the mutated value; `this.shelf` still holds what the property contained when the alias was created. As of Cypress 16 this is the default behaviour of every alias you create without options. ## Choosing between them - Prefer `cy.get('@name')` as the default. It stays inside the command queue, it re-derives DOM subjects against the current page, and it works in arrow-function tests, which most codebases' lint rules already prefer. - Reach for `this.name` when you want the value the setup produced regardless of what the test has done since — typically fixture data loaded in a hook and consumed as plain JavaScript. - If you want `cy.get('@name')` to behave the way `this.name` does, register the alias with `{ type: 'static' }`; Cypress then stores the resolved value instead of the chain and the two agree. - Do not mix the two styles for one alias in one spec. The freshness difference is invisible at the call site, and a later reader has no way to tell which semantics a line depends on. Neither read path is a way to carry something forward: both vanish together when Cypress resets state before the next test.

  • Why is this.books undefined on the line right after .as('books') in a Cypress test?
    Because `.as()` is enqueued, not executed inline. The test body only builds the command queue, and Cypress drains it after the body returns, so at the moment the next line of the body runs nothing has been written to Mocha's context yet. Read the value inside a `.then()` callback, or with `cy.get('@books')` — both run once the queue reaches them.
  • Does the type option on Cypress's .as() change how this.name behaves?
    No. `this.name` is always the value resolved when `.as()` ran, whichever type you pass; the option changes what `cy.get('@name')` does. The default `{ type: 'query' }` stores the chain and replays it on every read, while `{ type: 'static' }` stores the resolved value, which makes `cy.get('@name')` agree with `this.name`.

saying these in an interview costs you the question

  • Says this.name and cy.get('@name') are interchangeable
  • Writes an arrow-function test and expects this.name to work
  • Reads this.name on the line after calling .as()
  • Thinks cy.get('@name') returns a cached element reference