ScreenshotNeo

BlogHow-to

How to Test a Redux Store With Cypress

Test Redux-connected React components with Cypress using a fresh store and Provider. Learn when to seed state, test through the UI, or inspect the store directly.

By the ScreenshotNeo team4 October 20269 min read

For a Cypress component test, create a fresh Redux store for the test, wrap the component in React-Redux’s Provider, and assert what the component renders or does after user interaction. Seed the store with preloaded state or dispatch setup actions before mounting when a scenario needs a particular starting state. For end-to-end tests, normally exercise the running application through its visible UI and browser behavior; expose the Redux store to Cypress only when direct inspection or dispatch is genuinely useful.

This approach tests the component and real Redux logic together. It avoids shared mutable state between tests and keeps assertions focused on behavior a user can observe. The examples below use Redux Toolkit, React, and Cypress component testing. Cypress’s React examples show the Provider-wrapped mount pattern; Redux’s testing guidance recommends integration tests with a real store and mocked external requests where needed.

1. Choose the right test boundary

Test type Use it for Redux setup Prefer asserting
Cypress component test A focused connected component or small component flow Mount with a Provider and a new store; optionally seed state Rendered content and user-visible response to interactions
Cypress end-to-end test A workflow through the running application Use the app’s normal store setup; expose the store only for a justified diagnostic or hard-to-reach scenario Public UI behavior, network effects, and browser-visible outcomes
Pure unit test Complex reducer or selector logic that benefits from isolated inputs Call the pure function directly Its return value for defined inputs

Do not collapse component tests and end-to-end tests into one pattern. Component tests need a way to construct the connected component’s context. End-to-end tests can usually let the application initialize Redux as it does for users. In either case, avoid mocking React-Redux hooks or selector imports for ordinary behavior tests: that bypasses the integration you want confidence in.

2. Build a store factory and a Cypress mount command

A store factory creates a new store on each call. This is important because Redux stores are mutable: reusing one store across tests can leak state from one case into another. Here is a minimal example with a counter slice.

// src/store.js
import { configureStore, createSlice } from '@reduxjs/toolkit'

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    incremented(state) {
      state.value += 1
    },
    decremented(state) {
      state.value -= 1
    },
  },
})

export const { incremented, decremented } = counterSlice.actions

export function setupStore(preloadedState) {
  return configureStore({
    reducer: { counter: counterSlice.reducer },
    preloadedState,
  })
}

// Keep a normal application store separate from test setup if your app needs one.
export const store = setupStore()

The mount command wraps a component in a Provider and accepts an optional store. Its default is created by the factory, so a test that does not provide a store still gets an isolated instance.

// cypress/support/component.jsx
import { mount } from 'cypress/react'
import { Provider } from 'react-redux'
import { setupStore } from '../../src/store'

Cypress.Commands.add('mount', (component, options = {}) => {
  const { reduxStore = setupStore(), ...mountOptions } = options
  return mount(
    <Provider store={reduxStore}>{component}</Provider>,
    mountOptions
  )
})

Import the support file from your Cypress component configuration so the custom command is registered. Cypress’s mount command supports custom wrappers and configuration; follow the setup for your installed Cypress version in its mount API documentation. If you use TypeScript, add a declaration for the custom command in a file Cypress includes:

// cypress.d.ts
import 'cypress'
import type { MountOptions } from 'cypress/react'
import type { EnhancedStore } from '@reduxjs/toolkit'

declare global {
  namespace Cypress {
    interface Chainable {
      mount(
        component: React.ReactNode,
        options?: MountOptions & { reduxStore?: EnhancedStore }
      ): Chainable<unknown>
    }
  }
}

export {}

Adjust the store type and mount return type to match your project’s Cypress and Redux Toolkit versions. If the application’s store uses middleware, reducers, or enhancers, build them in setupStore just as the production store does. The test should use real application logic, not a reduced mock store that skips the reducers under test.

3. Test a connected component through its UI

For a component test, interact with the page and check its visible result. The component below reads state and dispatches an action through React-Redux:

// src/Counter.jsx
import { useDispatch, useSelector } from 'react-redux'
import { incremented } from './store'

export function Counter() {
  const value = useSelector((state) => state.counter.value)
  const dispatch = useDispatch()

  return (
    <section>
      <p>Count: <span data-cy="count">{value}</span></p>
      <button onClick={() => dispatch(incremented())}>Add one</button>
    </section>
  )
}
// cypress/component/Counter.cy.jsx
import { Counter } from '../../src/Counter'

describe('Counter', () => {
  it('shows the initial value and updates after a click', () => {
    cy.mount(<Counter />)

    cy.get('[data-cy="count"]').should('have.text', '0')
    cy.contains('button', 'Add one').click()
    cy.get('[data-cy="count"]').should('have.text', '1')
  })
})

This checks the component, Provider connection, reducer, and user interaction together. Cypress retries assertions while the app updates, so assert the expected DOM state rather than adding a fixed sleep.

4. Set up the starting state for a scenario

There are two useful setup methods. Use preloaded state when the scenario is defined by a starting snapshot. Dispatch actions on a newly created store when the scenario is more naturally expressed as application events.

Option A: pass preloaded state

it('renders a nonzero initial value', () => {
  const reduxStore = setupStore({ counter: { value: 7 } })

  cy.mount(<Counter />, { reduxStore })
  cy.get('[data-cy="count"]').should('have.text', '7')
})

The preloaded state’s shape must match the reducer keys and state shape. With persisted state, nested slices, or dynamically injected reducers, construct a complete valid snapshot. A partial object is not automatically merged at every nested level; use the initialization behavior your reducers expect.

Option B: dispatch setup actions before mounting

it('renders state created by an action', () => {
  const reduxStore = setupStore()
  reduxStore.dispatch(incremented())
  reduxStore.dispatch(incremented())

  cy.mount(<Counter />, { reduxStore })
  cy.get('[data-cy="count"]').should('have.text', '2')
})

Dispatching before mount keeps setup deterministic and makes the initial condition clear. If the test is specifically about a user triggering an action, mount the component first and use Cypress to interact with it instead.

5. Test asynchronous Redux behavior at the network boundary

For a thunk or other async flow, exercise the real component and Redux logic while controlling the external response. Intercept the HTTP request rather than mocking the selector, hook, or thunk implementation. The route and response below are illustrative; adapt them to the application’s API contract.

it('shows the fetched user name', () => {
  cy.intercept('GET', '/api/users/42', {
    statusCode: 200,
    body: { id: 42, name: 'Ada Lovelace' },
  }).as('getUser')

  cy.mount(<UserProfile userId="42" />)
  cy.contains('button', 'Fetch user').click()

  cy.wait('@getUser')
  cy.contains('Ada Lovelace').should('be.visible')
})

To test loading or failure states, configure the intercepted response accordingly and assert the status or error the user sees. Keep the thunk and reducer path active so the test covers the real state transition. Redux recommends mocking requests at the network boundary, for example with a request interception tool such as Mock Service Worker, when this keeps application code unchanged.

6. Use direct Redux access in end-to-end tests sparingly

Cypress end-to-end tests normally work through the browser-facing interface: DOM, network, and storage. If a test has a concrete reason to inspect or dispatch Redux state, the application must deliberately expose a reference. Cypress’s FAQ describes this as possible when the app makes the reference available.

One controlled pattern is to expose the store only in a test build or when a test flag is enabled. Do not place the store on the global object unconditionally in production.

// In application bootstrap code, guarded by your test-build configuration
if (import.meta.env.MODE === 'test') {
  window.__APP_STORE__ = store
}
// cypress/e2e/counter.cy.js
declare global {
  interface Window {
    __APP_STORE__?: {
      getState: () => unknown
      dispatch: (action: unknown) => unknown
    }
  }
}

describe('Redux diagnostic', () => {
  it('reads state after the user updates the counter', () => {
    cy.visit('/counter')
    cy.contains('button', 'Add one').click()

    cy.window().its('__APP_STORE__').should('exist').then((appStore) => {
      expect(appStore.getState()).to.deep.include({
        counter: { value: 1 },
      })
    })
  })
})

In a JavaScript spec, omit the TypeScript declare global block or move the type declaration into a TypeScript support file. The example is diagnostic: the user-visible assertion should usually remain the primary check. If the test directly dispatches an action, it can skip the UI path that would normally cause it, so reserve that technique for setup or a specific integration need.

7. Troubleshooting

Symptom Likely cause Fix
could not find react-redux context value The component was mounted without a Provider, or a different mount command is being used. Confirm the custom command wraps the component and that the Cypress support file registers it. Use the custom cy.mount in the spec.
State from another test appears unexpectedly A module-level store instance is reused. Call a store factory for each test. Pass a new store explicitly when seeding a scenario.
Preloaded state is missing or malformed The object does not match the configured reducer keys, or a nested slice has the wrong shape. Compare the snapshot to the root reducer shape and each slice’s initial state. Prefer dispatching setup actions if they better represent the desired condition.
Click succeeds but the expected value never appears The component may not use the injected store, the action/reducer wiring may be wrong, or the assertion targets stale or incorrect text. Check the Provider uses the same store passed to the test, confirm the reducer is registered, and assert the actual accessible or visible output.
Async test intermittently fails The test races the request or render update, or the response is not controlled. Intercept the request before mounting, wait for the aliased request, then assert the UI. Avoid arbitrary delay calls.
window.__APP_STORE__ is undefined The app did not expose it in the current build, or the app has not initialized. Enable the explicit test-only exposure and visit the correct route. Check it is attached to the same window Cypress is querying.
TypeScript rejects cy.mount or the store option The custom command declaration is missing, excluded from TypeScript, or uses incompatible Cypress types. Include the declaration file in the TypeScript project and align its imports and return types with the installed Cypress version.

8. Reliability, runtime, and maintenance

  • Isolation: create one store per test. Do not rely on test order or reset a shared store as a substitute for creating a fresh one.
  • Determinism: fix external API responses at the network boundary and make state setup explicit. This reduces dependence on live services and timing.
  • Useful coverage: assert the output and interaction that matter to a user. Add direct state checks only when they answer a question the UI cannot reasonably answer.
  • Runtime: component tests usually avoid starting the whole app for each focused case, while end-to-end tests cover more application wiring. The dossier provides no measured timing comparison, so choose the boundary based on needed confidence rather than an assumed speedup.
  • Cost: Cypress Redux tests run in your existing test environment; the cited guidance gives no pricing or benchmark figures. Account for your own CI and Cypress setup rather than assuming a particular cost.
  • Maintenance: keep the test store factory aligned with production reducers and middleware. A test-only simplified store can let wiring regressions pass unnoticed.

9. Or skip the browser setup

If the task is capturing a page for a visual reference, audit artifact, or AI workflow rather than testing Redux behavior, ScreenshotNeo is a website screenshot API and MCP server. A single GET request returns an image or PDF. For example, save a page as WebP:

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://example.com \
  -o shot.webp

See the ScreenshotNeo API documentation for parameters and response details. Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers identify the page verdict and billing status. An MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.

Create a free ScreenshotNeo account and start with 1,000 screenshots a month, no card required.

10. FAQ

Should I test the Redux store itself or the component using it?

For most app behavior, test the connected component with a real store and assert the resulting UI. Use focused unit tests for complex pure reducer or selector logic when those functions need isolated coverage.

Can I use Cypress component testing without Redux Toolkit?

Yes. The essential pattern is a fresh compatible Redux store and a React-Redux Provider. The store factory can use your application’s Redux setup regardless of whether it uses Redux Toolkit.

Can I check state with cy.window()?

Yes, if application code deliberately exposes the store reference. Keep that exposure controlled and use it only when public browser behavior does not answer the test’s question.

Does Cypress replace Redux unit tests?

No. Cypress covers browser-level or component integration behavior. Pure logic can still have small unit tests where isolating it makes the behavior clearer.

Primary references