skip to content

In Cypress, what turns a `cy.intercept()` route from a spy into a stub?

level: juniorimportance: must knowfreq 80%

answer

  1. Count the arguments you passed
  2. Matching, then what happens next
  3. No response argument means watch only
  4. String, object, StaticResponse or function
  5. Routes panel has a stubbed column

basics

~20 s

The response argument. A Cypress cy.intercept('GET', '/api/forecast') with no second argument only spies, and the request still reaches the real server. Supply a StaticResponse, a plain body, or a handler that calls req.reply() and Cypress answers it instead.

solid answer

~40 s

`cy.intercept()` decides between spying and stubbing on the argument that follows the matcher. `cy.intercept('GET', '/api/forecast')` registers a route with no handler: Cypress watches the request, counts it and lets it travel to the real server, so the route is only useful for observing and aliasing traffic. Pass anything as the response argument and Cypress fulfils the request itself: a string body, a plain object or array that becomes the JSON body, a `StaticResponse` such as `{ statusCode: 503, body: { message: 'stations offline' } }`, or `{ fixture: 'forecast.json' }`. A function handler sits in between — it always runs, but it only stubs if it calls `req.reply()` with a response. The Command Log's Routes panel carries a stubbed column per route, and a stubbed request's indicator is drawn unfilled rather than solid.

code

javascript · 16 lines
javascript
// spy only - the real forecast service answers
cy.intercept('GET', '/api/forecast*').as('forecast')

// stub - Cypress answers, the service is never contacted
cy.intercept('GET', '/api/stations', {
  statusCode: 200,
  body: [{ id: 'KSEA', name: 'Seattle-Tacoma' }],
})

// stub from a fixture file
cy.intercept('GET', '/api/alerts', { fixture: 'alerts.json' })

// handler that only edits the request: still a spy
cy.intercept('GET', '/api/forecast*', (req) => {
  req.headers['x-test-run'] = 'true'
})

go deeper

for a junior

Be ready to write both forms from memory and say which one lets the request reach the server. Knowing that the second argument is optional is the whole answer.

for a middle

Explain the four handler shapes - nothing, a string or object body, a StaticResponse, and a function - and what each one does to the outbound request.

for a senior

Show how you confirm a stub applied in a real run: the Routes panel's stubbed column, the filled versus unfilled request indicator, and what you check first when live data still appears.

for a principal

Own the convention for when a suite spies and when it stubs, so tests do not silently depend on a backend the team believes was faked.

## The argument that decides `cy.intercept()` takes up to three arguments: an optional HTTP method, a URL or `routeMatcher` that decides **which** requests the route claims, and an optional final argument that decides **what happens** to them. Only that last argument separates a spy from a stub. Everything before it is matching, and a route that matches without a response argument is a pure observer. ```js cy.intercept('GET', '/api/forecast') // spy: the server answers cy.intercept('GET', '/api/forecast', { body: forecast }) // stub: Cypress answers ``` A **spying** route registers itself, counts every matching request, records the request and the real response for later inspection, and can be given an alias with `.as()`. It changes nothing about the traffic: the application talks to exactly the backend it would have talked to with no test running. A **stubbing** route ends the request inside Cypress. The browser receives the response you described and the destination server is never contacted, so the test stops depending on that endpoint being up, seeded, or fast. ## What you can pass as the response | Response argument | What the application receives | |---|---| | *(nothing)* | the real response — spy only | | `'stations offline'` | that string as the body | | `{ id: 'KSEA', tempC: 14 }` | the object serialised as JSON, with `content-type: application/json` added for you | | `[{ id: 'KSEA' }, { id: 'KPDX' }]` | the array as the JSON body — an array is never read as options | | `{ statusCode: 503, body: { message: 'offline' } }` | a `StaticResponse`: your status code, body and headers | | `{ fixture: 'forecast.json' }` | the fixture file's contents as the body | | `(req) => { ... }` | whatever the handler decides — see below | A `StaticResponse` is the declarative form, and all of its properties are optional. `statusCode` defaults to `200`; `body` takes a string, object or `ArrayBuffer`; `headers` is a map of strings to strings; `fixture` names a file to serve as the body. `body` and `fixture` are mutually exclusive — set both and Cypress fails the command with "`body` and `fixture` cannot both be set, pick one." ## A function handler sits in between Passing a function does **not** automatically make the route a stub. The handler always runs, and what it does inside decides the outcome: - It **only inspects or edits `req`** and returns. The request continues to the real server, and any other matching route still gets its turn. This is still spying, with side effects on the request. - It calls **`req.reply(staticResponse)`**. Cypress answers, the server is never contacted, and the route is marked stubbed for that request. - It calls **bare `req.reply()` or `req.continue()`**. The request is sent outgoing and no further matching route runs. Not a stub — the completion call is about propagation, not about faking. - It calls **`req.continue((res) => { ... })`**. The request is sent for real, and the callback receives the genuine response to read or edit before the browser sees it. A hybrid: real traffic, shaped reply. That is why "I added a `cy.intercept()` with a callback and it still hits the server" is a correct description of Cypress rather than a bug report. ## Reading it back in the Command Log Cypress adds a **Routes** panel to the Command Log listing every registered route with its method, its matcher, its alias, whether the route is stubbed, and how many requests it has matched so far. Below that, each logged request carries a circular indicator: **filled** when the request went out to the destination server, **unfilled** when Cypress answered it from a stub. Clicking a request prints the full request and response objects to the browser console. Those two views answer "did my stub actually apply?" faster than any assertion, and they are the first place to look when a test that believes it is stubbed keeps reading live data. ## Choosing between the two 1. **Spy** when the assertion is about the *request*: that the dashboard asks for the station the user picked, sends the units query parameter, or does not refetch the forecast on every keystroke. 2. **Stub** when the *reply* is the point: an empty station list, a 503 from the alerts service, a forecast carrying a value the real backend will not produce on demand. 3. **Handler** when neither alone is enough: the reply has to be computed from the request, or the real response needs one field replaced on its way back. Note that `cy.intercept()` itself yields `null`, so nothing useful chains off it directly; a route is a registration, not a value. The most common mistake is assuming that interception implies stubbing. A route declared with no response argument is doing exactly what it was told to do — watching.

  • If a route stubs the response, does the handler still see the real response?
    No. A `StaticResponse` ends the request inside Cypress, so no request is sent and there is no real response to see. If you need the genuine response as well as a change to it, use a function handler and call `req.continue((res) => { ... })`, which sends the request for real and hands the callback the response on its way back.
  • What does Cypress do when two routes match the same request and only one supplies a response?
    Each matching route gets a turn until one of them completes the request. A route with no response argument, or a handler that only edits `req` and returns, falls through to the next one. The first route to supply a `StaticResponse` or call `req.reply()` ends the request phase, and routes behind it never run.

A spy is a wiretap on the line: the call still reaches the real switchboard and you only get the transcript. A stub cuts the line and answers the phone itself.

saying these in an interview costs you the question

  • Thinks cy.intercept always prevents the real request
  • Believes a function handler is automatically a stub
  • Calls bare req.reply() expecting an empty stub
  • Assumes a stub still needs the backend running