skip to content

In a Cypress component test, how do you assert that a component invoked its `onRate` callback prop?

level: juniorimportance: must knowfreq 78%

answer

  1. Inputs go in, callbacks come out
  2. Cypress bundles Sinon and sinon-chai
  3. A recording stand-in for the handler
  4. Give the double a name with .as()
  5. Assert through the @alias, not the variable

basics

~20 s

Create a Cypress spy with cy.spy(), name it with .as(), and pass it into cy.mount() as the prop. Drive the component, then assert through the alias: cy.get('@onRate').should('have.been.calledWith', 4). The alias is what makes the assertion retry until the callback fires.

solid answer

~40 s

`cy.spy()` with no arguments returns a bare Sinon spy that you hand to a component like any other prop. Create it, name it with `.as()`, then pass it through the mount: `const onRate = cy.spy().as('onRate')` followed by `cy.mount(<RatingStars value={2} onRate={onRate} />)`. Vue and Svelte take the same spy through the mount options, as `{ props: { onRate } }`. Drive the widget the way a user would, `cy.get('[data-cy=star-4]').click()`, and assert against the **alias** rather than the local variable: `cy.get('@onRate').should('have.been.calledWith', 4)`. Cypress bundles Sinon and sinon-chai, so `have.been.called`, `have.been.calledOnce` and `have.been.calledWith` work with no setup, and spies live in a sandbox Cypress resets between tests. Reach for `cy.stub()` instead when you also need to control what the prop returns.

code

javascript · 12 lines
javascript
import { RatingStars } from './RatingStars'

it('reports the chosen rating through onRate', () => {
  const onRate = cy.spy().as('onRate')

  cy.mount(<RatingStars value={2} max={5} onRate={onRate} />)

  cy.get('[data-cy=star-4]').click()

  cy.get('@onRate').should('have.been.calledOnce')
  cy.get('@onRate').should('have.been.calledWith', 4)
})

go deeper

for a junior

Be ready to type the four lines from memory: create the spy, alias it, pass it to the mount, assert on the alias. Interviewers often ask you to write this rather than describe it.

for a middle

Expect to explain why cy.spy() is synchronous while the assertion on its alias is not, and what sinon-chai adds on top of plain Chai.

for a senior

Show how you keep spy assertions specific enough to be worth having: calledWith over called, matchers for the arguments you cannot pin, and a failure message that names the case.

for a principal

Own the convention across the suite: one aliasing style, matchers rather than exact argument dumps, and a stated rule for when a stub's return value becomes part of the contract a component test pins.

A component test drives a component through its **inputs** and watches its **outputs**. The inputs are the props handed to the mount; the outputs that never appear on screen are the callbacks the component invokes. Cypress covers that second half with **spies** and **stubs** — recording stand-ins you pass in like any other prop and assert against afterwards. ## The two test doubles Cypress gives you - **`cy.spy()`** wraps a function without changing it: calls are recorded and the original still runs. Called with **no arguments** it returns a standalone spy, which is exactly what a prop such as `onRate` needs, because there is no real implementation to preserve inside the test. - **`cy.stub()`** *replaces* the function. Reach for it when you also need to control the answer, through the Sinon stub API that Cypress exposes: `cy.stub().returns(true)`, `.resolves(row)`, `.rejects(new Error('offline'))`, or `.withArgs(4).returns('four stars')`. Both are **synchronous utility functions, not commands or queries**. They are not retryable, not chainable off `cy`, and cannot be given a timeout — they simply return real Sinon objects. Cypress creates them inside a Sinon sandbox that it resets between tests, so there is nothing for you to tear down. ## Wiring the spy into the mount 1. Create and name the spy: `const onRate = cy.spy().as('onRate')`. `.as()` hands the spy back, so one line gives you both a local reference and a registered alias. 2. Pass it in exactly where the application would pass a real handler. 3. Drive the component through the DOM the way a user would: `cy.get('[data-cy=star-4]').click()`. 4. Assert against the **alias**: `cy.get('@onRate').should('have.been.calledWith', 4)`. Step 4 is the one people skip. A bare `expect(onRate).to.have.been.calledWith(4)` evaluates Chai once, at that instant. `cy.get('@onRate')` is a query, so Cypress re-reads the spy and re-runs the assertion until it passes or the command timeout expires — which is what you want the moment the callback is deferred by a re-render or a timer. ## How each adapter takes props | Adapter | Props / inputs | Callback spy | | --- | --- | --- | | `cypress/react` | props on the JSX: `cy.mount(<RatingStars value={2} onRate={onRate} />)` | pass the spy straight in as the prop | | `cypress/vue` | `props` in the mount options | pass the spy as a prop | | `cypress/svelte` | `props` in the mount options | pass the spy as a prop | | `cypress/angular` | `componentProperties` in the mount config | `createOutputSpy('onRate')` from the `cypress/angular` package, because an Angular `@Output()` is an EventEmitter rather than a plain function | ## The assertion vocabulary Cypress bundles Sinon together with **sinon-chai**, which teaches Chai about spies. The chainers you will use daily, in their `.should()` form: - `should('have.been.called')` — at least one call happened - `should('have.been.calledOnce')`, `calledTwice`, `calledThrice` - `should('have.callCount', 3)` — an exact number of calls - `should('have.been.calledWith', 4)` — some call received these arguments - `should('have.been.calledWithExactly', 4)` — some call received these and **only** these - `should('have.been.calledOnceWith', 4)` — exactly one call, and it received these - `should('have.been.calledWithMatch', { source: 'keyboard' })` — partial match against an object argument When one argument matters and another does not, use Sinon's matchers through `Cypress.sinon`: `cy.get('@onRate').should('have.been.calledWith', 4, Cypress.sinon.match.object)` pins the rating and stays loose about the event object beside it. ## Narrowing a spy with `withArgs` Sinon's `.withArgs()` produces a second spy that records only the calls matching those arguments, and it can carry an alias of its own: ```js const onRate = cy.spy().as('onRate') const rateFour = onRate.withArgs(4).as('rateFour') ``` `cy.get('@rateFour').should('have.been.calledOnce')` now fails with a message about the four-star case specifically, rather than about every call the widget made. Cypress links the two in the Command Log, and clicking either entry prints the call count, the real untransformed arguments, the return value and the calling context. ## Mistakes worth naming - Asserting on the closure variable instead of the alias, then blaming the component when the result is intermittent. - Reaching for `cy.stub()` when the prop's real behaviour still has to run; `cy.spy()` is the non-invasive choice. - Resetting spies by hand in a `beforeEach`, when the sandbox already does it for you. - Settling for `have.been.called` where `have.been.calledWith` would have caught the widget passing the wrong value.

  • When would you pass cy.stub() to a prop instead of cy.spy()?
    `cy.spy()` only records calls and lets the original run, so it fits a prop the test supplies anyway. Use `cy.stub()` when the component depends on what the prop returns — a `confirmDelete` prop the data grid awaits, for example. `cy.stub().returns(true)` and `cy.stub().rejects(new Error('offline'))` let you drive the happy path and the failure path from the same test.
  • How do you assert on one argument you care about and ignore another?
    Use a Sinon matcher through `Cypress.sinon`: `cy.get('@onRate').should('have.been.calledWith', 4, Cypress.sinon.match.object)`. The matchers include `match.string`, `match.number`, `match.object` and `match(fn)`, so you can pin the value that matters and stay loose about the event object beside it. `have.been.calledWithMatch` does the same job for a partial object comparison.

saying these in an interview costs you the question

  • Asserting on the spy variable instead of its alias, so nothing retries
  • Treating cy.spy() as a chainable Cypress command with a timeout
  • Believing spies must be reset by hand between tests
  • Using cy.stub() when the prop's real implementation still needs to run