How to Fix Cypress Component Test Stubs with RTK Query
Fix Cypress RTK Query stubs by wiring the store, matching the real request, returning the right shape, and waiting for the UI state.
Most Cypress Component Testing failures with RTK Query come from one of five causes: the component is mounted without its Redux API context, the intercept is registered too late, the route does not match the real request, the fixture has the wrong shape, or the test asserts before the request finishes.
Mount the real component with a fresh store that includes the RTK Query reducer and middleware. Register cy.intercept() before mounting or triggering the query, match the exact method and URL, return the response shape the endpoint expects, wait on the route alias, and then assert on a user-visible state.
This guide shows a complete setup, explains why each step matters, and provides a decision table for requests that never match.
1. Build a fresh Redux store for every component test
RTK Query hooks read from Redux. Your Cypress mount helper must provide the configured store, including the API reducer and middleware. Create a new store for each test so cached query data and loading state cannot leak between tests.
import { configureStore } from '@reduxjs/toolkit'
import { Provider } from 'react-redux'
import { api } from '../../src/services/api'
import { mount } from 'cypress/react'
export function makeStore() {
return configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
})
}
Cypress.Commands.add('mountWithStore', (component) => {
const store = makeStore()
return cy.mount(
<Provider store={store}>
{component}
</Provider>,
)
})
Use the helper in the test. Cypress’s React examples demonstrate Redux-backed mounting and recommend initializing a fresh store for each test: Cypress React Component Testing.
import UserCard from '../../src/components/UserCard'
describe('UserCard', () => {
it('renders the user returned by RTK Query', () => {
cy.mountWithStore(<UserCard userId="42" />)
})
})
Check the API slice registration
The reducer key must use api.reducerPath, not a guessed string. The middleware must be appended to the default middleware. Omitting either can produce missing-context errors, subscriptions that never resolve, or queries that do not update the component.
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.test' }),
endpoints: (builder) => ({
getUser: builder.query({
query: (id) => `/users/${id}`,
}),
}),
})
2. Register the intercept before the request can happen
Define the intercept before cy.mount() when the hook fetches during render. For queries triggered by a click, define it before the click. Cypress intercepts front-end HTTP requests and clears intercepts between tests: cy.intercept() API.
it('renders the stubbed user', () => {
cy.intercept('GET', 'https://example.test/users/42', {
statusCode: 200,
body: {
id: '42',
name: 'Ada Lovelace',
},
}).as('getUser')
cy.mountWithStore(<UserCard userId="42" />)
cy.wait('@getUser')
cy.contains('Ada Lovelace').should('be.visible')
})
If your application uses a relative base URL, match the relative path. If it constructs an absolute URL, match that URL or use a glob that still constrains the endpoint:
cy.intercept('GET', '**/users/42').as('getUser')
Use a route object when query parameters matter:
cy.intercept({
method: 'GET',
url: '**/users',
query: { id: '42' },
}, { statusCode: 200, body: { id: '42', name: 'Ada Lovelace' } }).as('getUser')
3. Return the response shape the endpoint expects
A 200 response can still produce an error or empty UI when its JSON shape differs from the endpoint’s expected data. RTK Query can also transform a response before caching it, so inspect both transformResponse and the component’s selectors.
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.test' }),
endpoints: (builder) => ({
getUser: builder.query({
query: (id) => `/users/${id}`,
transformResponse: (response) => response.user,
}),
}),
})
For that endpoint, the fixture must contain { "user": { ... } }, not only the inner user object:
cy.intercept('GET', '**/users/42', {
statusCode: 200,
body: {
user: { id: '42', name: 'Ada Lovelace' },
},
}).as('getUser')
Also stub the status and headers your code relies on. For an error state, return the same error format your base query produces:
cy.intercept('GET', '**/users/42', {
statusCode: 404,
body: { message: 'User not found' },
}).as('getUser')
4. Wait for the request, then assert on the rendered state
cy.mount() returns before asynchronous rendering and network work have completed. Waiting on the alias confirms that the request matched and completed. Follow it with a retryable assertion against what the user sees.
cy.wait('@getUser').its('response.statusCode').should('eq', 200)
cy.get('[data-cy=user-name]').should('have.text', 'Ada Lovelace')
Do not use a fixed sleep as the primary synchronization mechanism. A sleep can be too short on a busy run and unnecessarily slow when the response is immediate.
5. Complete Cypress component test example
// src/services/api.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.test' }),
endpoints: (builder) => ({
getUser: builder.query<{ id: string; name: string }, string>({
query: (id) => `/users/${id}`,
}),
}),
})
// cypress/support/component.tsx
import './commands'
// cypress/support/commands.tsx
import { configureStore } from '@reduxjs/toolkit'
import { Provider } from 'react-redux'
import { api } from '../../src/services/api'
Cypress.Commands.add('mountWithStore', (component) => {
const store = configureStore({
reducer: { [api.reducerPath]: api.reducer },
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
})
return cy.mount(<Provider store={store}>{component}</Provider>)
})
// UserCard.cy.tsx
import UserCard from './UserCard'
describe('UserCard', () => {
it('shows a loaded user', () => {
cy.intercept('GET', '**/users/42', {
statusCode: 200,
body: { id: '42', name: 'Ada Lovelace' },
}).as('getUser')
cy.mountWithStore(<UserCard userId="42" />)
cy.get('[data-cy=loading]').should('be.visible')
cy.wait('@getUser')
cy.get('[data-cy=user-name]')
.should('be.visible')
.and('have.text', 'Ada Lovelace')
})
it('shows an error state', () => {
cy.intercept('GET', '**/users/42', {
statusCode: 500,
body: { message: 'Server error' },
}).as('getUser')
cy.mountWithStore(<UserCard userId="42" />)
cy.wait('@getUser')
cy.get('[data-cy=error]').should('be.visible')
})
})
6. Diagnose an intercept that never matches
| Symptom | Likely cause | Fix |
|---|---|---|
| Missing context or store error | The component is mounted without the Redux provider, API reducer, or middleware. | Use a mount helper with a configured store and create it inside each test. |
cy.wait('@request') times out |
The intercept was registered after the request, the method or URL differs, the endpoint never ran, or the response came from browser cache. | Register before mount/action; inspect the request; match the full path, query string, and method; confirm the endpoint uses browser HTTP. |
| Intercept matches but UI is empty | Fixture shape, status, transformation, or assertion timing is wrong. | Compare the fixture with the endpoint and transformResponse; wait for the alias before asserting. |
| Passes alone but fails in the suite | A shared Redux store retained RTK Query cache or subscriptions. | Create a fresh store for every test and avoid module-level store instances. |
| Request appears twice | React development behavior, retries, refetch-on-mount, or two mounted consumers can issue multiple requests. | Inspect all matches, disable retries for the test if configured, and assert the intended request rather than assuming one call. |
| Request URL is correct but Cypress sees nothing | The endpoint uses a custom queryFn or baseQuery that resolves data without browser HTTP. |
Test that function at its own seam or mock the underlying transport; cy.intercept() only observes browser network traffic. |
Inspect the real request
Temporarily spy without stubbing to discover the exact method, URL, query string, headers, and request count:
cy.intercept('**', (request) => {
console.log(request.method, request.url)
}).as('anyRequest')
cy.mountWithStore(<UserCard userId="42" />)
cy.wait('@anyRequest')
Prefer a narrow diagnostic pattern in the final test. A broad ** route can catch unrelated requests and hide a bad matcher.
7. Browser cache, retries, and RTK Query cache
A browser-cached response does not pass through Cypress’s network interception layer. If no request appears, disable caching for the application under test or change the query input so the browser must request a new resource.
RTK Query also caches successful query data in Redux. A reused store can make a component render immediately without issuing a request, which explains tests that pass alone but fail when run together. A per-test store is the simplest isolation boundary.
When retries are enabled, a failed request can produce several intercepted calls. Wait for the alias and assert the final UI state; if call count is part of the behavior, assert it explicitly after the retry policy has finished.
8. Custom baseQuery and queryFn seams
RTK Query permits arbitrary asynchronous request handling. A custom base query must convert failures into an { error } result and successes into an { data } result.
const customBaseQuery = async (args) => {
try {
const data = await client.request(args)
return { data }
} catch (error) {
return {
error: {
status: 'CUSTOM_ERROR',
error: String(error),
},
}
}
}
If this code calls an SDK directly, there may be no browser request for Cypress to intercept. Keep the component test focused on Redux and rendering, and unit test the custom transport separately, or expose an HTTP seam that Cypress can observe.
9. Cypress interception versus MSW
For a Cypress component test that should exercise the real RTK Query hook and browser request, cy.intercept() is a natural seam. It scopes the stub to Cypress, lets you wait on the exact request, and can return per-test responses.
Redux’s integration testing guidance demonstrates Mock Service Worker (MSW) handlers for component tests: Redux writing tests. MSW can be a better fit when the same handlers must run in multiple environments, such as unit tests, component tests, and development. Neither choice removes the need for correct Redux setup, route matching, and response shape.
| Question | cy.intercept() |
MSW |
|---|---|---|
| Where does the mock apply? | Cypress browser traffic. | Any environment where the MSW worker or server is configured. |
| Best for | Verifying the browser request and synchronizing with it. | Reusable handlers shared across test environments. |
| Still required | Correct URL, method, timing, and fixture shape. | Correct handler matching and fixture shape. |
10. Performance, reliability, and cost notes
- Use a fresh store per test, but keep the store factory small so setup does not dominate the suite.
- Match the narrowest URL pattern that represents the endpoint. Broad intercepts can capture unrelated assets and make failures harder to interpret.
- Wait on network aliases instead of arbitrary delays to reduce runtime and flakiness.
- Use deterministic fixtures for loading, success, empty, and error states. Keep one test focused on one state transition.
- When testing real backend behavior, use a separate integration suite. Component stubs should not depend on backend availability.
- RTK Query cache behavior is part of the component’s runtime contract. Add a focused test for refetch or invalidation instead of relying on accidental cache state.
11. A practical debugging checklist
- Does the component render inside a Redux
Provider? - Does the store contain
api.reduceratapi.reducerPath? - Is
api.middlewareincluded? - Is the store created inside the test or mount helper?
- Is
cy.intercept()registered before mount or the triggering action? - Do method, origin, path, query parameters, and URL encoding match?
- Does the endpoint use
fetchBaseQuery, another browser transport, or a custom function? - Does the stub body match the endpoint’s input and
transformResponse? - Have you waited for the alias before asserting?
- Could browser cache or RTK Query cache explain the missing request?
- Are retries or multiple consumers causing more than one request?
12. Or skip the browser setup
If the goal is to capture a stable screenshot of a test page, report, or UI state rather than maintain a browser harness, ScreenshotNeo provides a single HTTP request. Its API can wait for a selector or delay, run custom JavaScript, set headers and cookies, choose a viewport or device preset, capture an element or full page, and return PNG, JPEG, WebP, or PDF.
cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
See the ScreenshotNeo API documentation for request options. Cookie banners, newsletter popups, and chat widgets are removed before the shot; bot checks, blank pages, timeouts, failed loads, and cache hits are never billed; response headers identify the page verdict and billing status. Its MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots each month with no card, and paid plans start at $5 for 3,000 shots.
Create a free ScreenshotNeo account and start with 1,000 screenshots a month at no charge.
FAQ
Why does my intercept work for a full end-to-end test but not component testing?
Component tests often use a different base URL, mount helper, or environment configuration. Log the actual request from the component test and match that method and URL.
Should I mock the RTK Query hook directly?
Usually no when you want to verify Redux integration and request behavior. Intercept the browser request or use shared MSW handlers. Mock the hook directly only when the request layer is outside the component test’s scope.
Can I reuse one Redux store across tests?
Do not reuse it for isolated component tests. RTK Query cache, subscriptions, and invalidation state can change later tests.
What does a successful cy.wait() prove?
It proves the aliased route matched and completed. It does not prove that the fixture has the shape your component expects, so keep a UI assertion after the wait.
Why is there no network request when the hook runs?
The result may come from browser cache or RTK Query cache, or the endpoint may use a custom queryFn or baseQuery that does not issue browser HTTP.


