Skip to content

Cypress Spies

A spy lets you observe how a function is called without altering what it actually does. This lesson explains cy.spy(), how it differs from cy.stub(), and when to reach for each.

cy.spy(): Observe Without Replacing

cy.spy() wraps a function so Cypress records every call, its arguments, return value, and call count, while still letting the original implementation run normally. This differs from cy.stub(), which replaces the implementation entirely.

const calculateTotal = (items) => items.reduce((sum, i) => sum + i.price, 0);

cy.spy(window, 'calculateTotal').as('calcSpy'); // observes real calls

// later
cy.get('@calcSpy').should('have.been.called');

The real calculateTotal still runs and returns its actual value, cy.spy() only observes the call.

cy.spy() Syntax

cy.spy(object, 'methodName')
cy.spy(object, 'methodName').as('name')
  • cy.spy() requires an existing object and method name; unlike cy.stub(), it cannot create a bare standalone function.
  • The wrapped method's real implementation still executes exactly as before.
  • Aliasing with .as() enables later assertions with cy.get('@alias').
  • Spies are automatically restored to the original implementation after the test.

Spy vs Stub Cheat Sheet

Deciding between cy.spy() and cy.stub() for a given scenario.

Scenario Use
Verify a callback was called, real behavior unaffected cy.spy()
Replace a function's actual behavior entirely cy.stub()
Verify a callback was called with specific arguments Either, depending on if real behavior matters
Prevent a real side effect (e.g., real API call) from happening cy.stub()
Confirm an internal method is invoked during a real flow cy.spy()

A Practical Example: Spying on console.error

Spying on console.error is a useful pattern for confirming your application does not silently log unexpected errors during a flow you're testing, while still letting any real logging behavior happen normally.

cy.window().then((win) => {
  cy.spy(win.console, 'error').as('consoleError');
});

cy.get('[data-cy="submit"]').click();
cy.get('@consoleError').should('not.have.been.called');

Choosing Between a Spy and a Stub

The deciding question is simple: do you need the real implementation to actually run? If yes, and you only need to observe the call, use cy.spy(). If the real implementation is unwanted, expensive, or has side effects you need to prevent (like a real payment call), use cy.stub() instead.

Common Mistakes

  • Trying to use cy.spy() to create a brand-new standalone function the way cy.stub() can.
  • Using cy.stub() when the real implementation's actual behavior was still needed for the test to be meaningful.
  • Forgetting spies observe an existing object's method, and require that method to already exist.
  • Not aliasing a spy, making it impossible to assert on its calls afterward.

Key Takeaways

  • cy.spy() observes calls to an existing function while letting its real implementation run.
  • cy.stub() replaces a function's implementation entirely.
  • Choose based on whether the real behavior of the function under test still needs to execute.
  • Both spies and stubs support Sinon-Chai assertions like have.been.called and calledWith.

Pro Tip

Reach for cy.spy() first by default when you just need to confirm "was this called, and how", only escalate to cy.stub() once you've identified a concrete reason the real implementation shouldn't run during that specific test.