---
title: "Testing Redux Store"
url: "https://www.cypress.io/blog/testing-redux-store"
description: "Learn how to run assertions against a React application that uses a Redux store."
---

# Testing Redux Store

November 14, 2018

By Gleb Bahmutov

![Image of a frozen lake in the winter with ice that is starting to break into pieces](https://storage.ghost.io/c/12/89/12899ca1-4786-4eea-b7e1-9c5268baa48a/content/images/2025/04/Photo-1530110023-1080x1440.jpeg)

Cypress tests usually operate via public browser APIs like DOM, network, or storage. But the tests can just as easily reach into the application and check if the internal state is updated correctly. In this blog post I will show you how to run assertions against a React application that uses a [Redux](https://redux.js.org/?ref=cypress-io.ghost.io) store.

This blog post assumes you’ve already [installed Cypress](https://on.cypress.io/installing-cypress?ref=cypress-io.ghost.io), and you can find the source code in [cypress-example-recipes#testing-redux-store](https://github.com/cypress-io/cypress-example-recipes?ref=cypress-io.ghost.io#testing-redux-store).

## [](#First-test "First test")First test

I have copied the official Redux TodoMVC example from [reduxjs/redux/tree/master/examples/todomvc](https://github.com/reduxjs/redux/tree/master/examples/todomvc?ref=cypress-io.ghost.io). This application runs on port `3000`, so I set this as our [base url](https://on.cypress.io/configuration?ref=cypress-io.ghost.io#Global) in my `cypress.json` file.

```json
{
  "baseUrl": "http://localhost:3000"
}
```

The first test just confirms that the application loads, has an input field and 1 todo.

```js
// cypress/integration/spec.js
it('loads', () => {
  cy.visit('/')
  cy
  .focused()
  .should('have.class', 'new-todo')
  .and('have.attr', 'placeholder', 'What needs to be done?')
  cy.get('.todo-list li').should('have.length', 1).contains('Use Redux')
})
```

## Access Redux store

Great, now let’s expose the application’s store - but only if the application is running inside our Cypress tests. In my `src/index.js` file, let’s add an `if (window.Cypress)` block:

```js
const store = createStore(reducer)

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

// expose store when run in Cypress
if (window.Cypress) {
  window.store = store
}
```

Now open your DevTools console in the browser while [running Cypress](https://on.cypress.io/command-line?ref=cypress-io.ghost.io#cypress-open). Within your DevTools, switch to “Your App” context using the drop down (underlined in the image below) and you should be able to get to the Redux store and see its current state.

If we can get to the store from the DevTools console - we can get to it from our Cypress test. We just need to get the reference to the application’s `window` context. We can do this using the [`cy.window()`](https://on.cypress.io/window?ref=cypress-io.ghost.io) command. From the yielded `window` object we can grab the `store` property and invoke `getState` to get the current state object.

Let’s confirm that the store has the expected state when the application loads.

```js
it('has expected state on load', () => {
  cy.visit('/')
  cy.window().its('store').invoke('getState').should('deep.equal', {
    todos: [
      {
        completed: false,
        id: 0,
        text: 'Use Redux',
      },
    ],
    visibilityFilter: 'show_all',
  })
})
```

The test passes and if we click on the `INVOKE .getState()` command in the [Command Log](https://on.cypress.io/test-runner?ref=cypress-io.ghost.io#Command-Log), we can inspect the dumped state object in the DevTools console. Most of Cypress’s commands print additional information to the console when you click on the Command Log.

## Drive using the DOM

We can drive the application using the DOM and then assert the state changes as we expect. Let us add a second todo item, mark it as completed, change the filter and then check the store.

```js
it('is updated by the DOM actions', () => {
  cy.visit('/')
  cy.focused().type('Test with Cypress{enter}')
  cy.contains('li', 'Test with Cypress').find('input[type=checkbox]').click()
  cy.contains('.filters a', 'Completed').click()
  cy.window().its('store').invoke('getState').should('deep.equal', {
    todos: [
      {
        completed: false,
        id: 0,
        text: 'Use Redux',
      },
      {
        completed: true,
        id: 1,
        text: 'Test with Cypress',
      },
    ],
    visibilityFilter: 'show_completed',
  })
})
```

**Pro tip 💡:** Cypress tests run very quickly. To see what is going on while working on a test, add a [`.pause()`](https://on.cypress.io/pause?ref=cypress-io.ghost.io) command to the test.

## A problem

Let’s take a look at the above test again. We are going to enter a couple of items using the application’s user interface and then check that the right number of items are in the store.

```js
it('can wait for delayed updates', () => {
  cy.visit('/')
  cy.focused().type('first{enter}').type('second{enter}')
  cy.window().its('store').invoke('getState').its('todos').should('have.length', 3)
})
```

There is a subtle race condition possible in the above code. Let’s say that our application handles events with a slight delay - which is possible in a typical application. Maybe the app is sending each new item to the server to be saved. In our application code we will simulate the delay by changing the `Header` component.

```jsx
// src/components/Header.jsx
// change from this
const Header = ({ addTodo }) => (
  <header className="header">
    <h1>todos</h1>
    <TodoTextInput
      newTodo
      onSave={(text) => {
        if (text.length !== 0) {
          addTodo(text)
        }
      }}
      placeholder="What needs to be done?"
    />
  </header>
)
// to this
const Header = ({ addTodo }) => (
  <header className="header">
    <h1>todos</h1>
    <TodoTextInput
      newTodo
      onSave={(text) => {
        if (text.length !== 0) {
          setTimeout(addTodo, 1000, text)
        }
      }}
      placeholder="What needs to be done?"
    />
  </header>
)
```

The only change was to delay calling the `addTodo` action by 1 second from `addTodo(text)` to `setTimeout(addTodo, 1000, text)`. Will our test break? Yes, watch it fail in the video below.

Notice the order of events in the command log:

1.  Type “first”.
2.  Type “second”.
3.  Get `window` reference.
4.  Get `store` property from the `window` object.
5.  Invoke `getState`.
6.  Get `todos` starts spinning.
7.  Items “first” and “second” appear in the list.
8.  Get `todos` command and assertion `should('have.length', 3)` fail after a few seconds.

In Cypress almost all assertions do NOT fail immediately. Instead, a failing assertion tells Cypress to go back to _the immediate previous_ command and rerun it - and then checks the assertion again. Still failing? Rerun the command and check again! This continues for up to 4 seconds by default. After the 4 second [`defaultCommandTimeout`](https://on.cypress.io/configuration?ref=cypress-io.ghost.io#Timeouts) ends with the assertion still failing, the command fails. This built-in command retry-ability is the [core feature of Cypress](https://on.cypress.io/introduction-to-cypress?ref=cypress-io.ghost.io). But not all commands in the test are retried - only the last one; and not every command can be retried: [`cy.get()`](https://on.cypress.io/get?ref=cypress-io.ghost.io#Assertions) and [`.its()`](https://on.cypress.io/its?ref=cypress-io.ghost.io#Assertions) can be safely retried, while [`.type()`](https://on.cypress.io/type?ref=cypress-io.ghost.io) and [`.invoke()`](https://on.cypress.io/invoke?ref=cypress-io.ghost.io) change the application and thus are NOT retried.

> I explain command retries in my “End-to-end Testing Is Hard - But It Doesn’t Have to Be” presentation at ReactiveConf 2018: [video](https://www.youtube.com/watch?v=swpz0H0u13k&ref=cypress-io.ghost.io), [slides](https://slides.com/bahmutov/reactive-conf?ref=cypress-io.ghost.io).

### Observing the DOM

We can solve the above race condition in two ways. First, we can make our test _wait_ for the application to actually process the action by observing an external effect - the DOM mutation for example. In our test, we grabbed the reference to the store and its state object _too early_, before the application had a chance to actually set the new state. We are holding onto a stale `state` object reference returned by the `invoke('getState')` call - it will never get updated, even when the application’s internal store gets the new items.

We need to _delay_ getting the state object reference; we need to get it after the application updates it. We can tell when the application does add the items to the store - it happens when the DOM is updated! So to make our test more robust, we need to assert the number of visible items in the DOM (which can use built-in retries), and _then get the Redux store_.

```js
it('can wait for delayed updates', () => {
  cy.visit('/')
  cy.focused().type('first{enter}').type('second{enter}')
  // check the dom
  cy.get('.todo-list li').should('have.length', 3)
  // now redux store should have been updated
  cy.window().its('store').invoke('getState').its('todos').should('have.length', 3)
})
```

Take a look at the passing test now.

Only when the DOM is updated (when the command and assertion `cy.get('.todo-list li').should('have.length', 3)` passes) will the next command `cy.window()` start running - and by then the store has been updated. Whenever a test is flaky, make the test observe an external change from the application. It could be a DOM update, a changed storage, or a network request. Then check the application’s internal data - it should be ready by then.

### Retrying custom code

We can solve the race condition in another way - by retrying the entire chain of code that gets us the list of todos, instead of just retrying the last command `its('todos')`. First, replace `cy.window().its('store').invoke('getState').its('todos')` with a custom function inside a [`.then()`](https://on.cypress.io/then?ref=cypress-io.ghost.io) callback. The test is still failing, but this is a start

```js
it('can wait for delayed updates using pipe', () => {
  cy.visit('/')
  cy.focused().type('first{enter}').type('second{enter}')
  const getTodos = (win) =>
    win.store.getState().todos
  cy.window().then(getTodos).should('have.length', 3)
})
```

Inside `getTodos` we are doing the same actions as before, but without using the Cypress commands, instead we are using plain JavaScript. The test is failing, because `.then()` is NOT retried. Cypress has no idea if the user callback is safe to retry! To really retry, you need to bring a user plugin called [cypress-pipe](https://github.com/NicholasBoll/cypress-pipe?ref=cypress-io.ghost.io) by [Nicholas Boll](https://github.com/NicholasBoll?ref=cypress-io.ghost.io).

> Nicholas Boll is awesome and has written most of the [Cypress TypeScript definitions](https://github.com/cypress-io/cypress/blame/develop/cli/types/index.d.ts?ref=cypress-io.ghost.io).

Install `cypress-pipe` using `npm i -D cypress-pipe` and add it to the `cypress/support/index.js`

```js
import 'cypress-pipe';
```

This will add a new command `.pipe()` that can replace `.then()` and will be retried!

```js
it('can wait for delayed updates using pipe', () => {
  cy.visit('/')
  cy.focused().type('first{enter}').type('second{enter}')
  const getTodos = (win) =>
    win.store.getState().todos
  // using cypress-pipe the "getTodos" will be retried until
  //   should('have.length', 3) passes
  //    or
  //   default command timeout ends
  cy.window().pipe(getTodos).should('have.length', 3)
})
```

The test passes after `getTodos` returns a list with 3 items. Notice in the animation how the command log flashes “Array(2)” before passing with the “Array(3)” assertion.

Ok, let us remove the artificial action delay from the application’s code before proceeding to the next section.

## Dispatch actions

We do not have to just drive the application via DOM events. We can dispatch Redux actions directly, and then check if the DOM has been updated correctly. We just need to know what actions our application expects. In `src/reducers/todos.js`, I see the following code:

```js
import {ADD_TODO, ...} from '../constants/ActionTypes'

export default function todos(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
          completed: false,
          text: action.text
        }
      ]
  ...
```

and in `src/constants/ActionTypes.js` I see that `ADD_TODO` is just a string `export const ADD_TODO = 'ADD_TODO'`. Great, let’s write a test using the [`.invoke()`](https://on.cypress.io/invoke?ref=cypress-io.ghost.io) command.

```js
it('can drive app by dispatching actions', () => {
  cy.visit('/')
  // dispatch Redux action
  cy
  .window()
  .its('store')
  .invoke('dispatch', { type: 'ADD_TODO', text: 'Test dispatch' })
  // check if the app has updated its UI
  cy.get('.todo-list li').should('have.length', 2).contains('Test dispatch')
})
```

Great, the DOM is in fact updated and shows the new text.

## Share code

Hmm, I do not want to duplicate actions in my specs, what if the constant `ADD_TODO` changes? It would be nice to use the actions already defined in the application code file `src/actions/index.js`.

```js
import * as types from '../constants/ActionTypes'

export const addTodo = text => ({ type: types.ADD_TODO, text })
export const deleteTodo = id => ({ type: types.DELETE_TODO, id })
...
```

Cypress includes a code bundler, so we can just import these constants from the spec file.

```js
import { addTodo, deleteTodo } from '../../src/actions'

it('can use actions from application code', () => {
  cy.visit('/')
  cy.window().its('store').invoke('dispatch', addTodo('Share code'))
  cy.window().its('store').invoke('dispatch', deleteTodo(0))
  cy.get('.todo-list li').should('have.length', 1).contains('Share code')
})
```

Let’s keep our code DRY and avoid duplicate commands. It is just JavaScript, and the test code should be as well engineered as the application code.

```js
import { addTodo, deleteTodo } from '../../src/actions'
const dispatch = action => cy.window().its('store').invoke('dispatch', action)

it('can use actions from application code', () => {
  cy.visit('/')
  dispatch(addTodo('Share code'))
  dispatch(deleteTodo(0))
  cy.get('.todo-list li').should('have.length', 1).contains('Share code')
})
```

> The spec code above looks imperative with `dispatch ...` calls, but all the Cypress commands are running one by one, and only if the previous command finishes successfully. I explain this declarative syntax and command chaining in the second half of the “End-to-end Testing Is Hard - But It Doesn’t Have to Be” ReactiveConf 2018 presentation [video](https://www.youtube.com/watch?v=swpz0H0u13k&ref=cypress-io.ghost.io), [slides](https://slides.com/bahmutov/reactive-conf?ref=cypress-io.ghost.io).

## A good start

Always starting our test with the same hardcoded todo “Use Redux” is weird. Let us change our application code to allow passing different initial store. In `src/reducers/todos.js` we will replace the code…

```js
const initialState = [
  {
    text: 'Use Redux',
    completed: false,
    id: 0
  }
]

export default function todos(state = initialState, action) {
  ...
```

…with a conditional initializer:

```js
const initialState = (window.Cypress && window.initialState) || [
  {
    text: 'Use Redux',
    completed: false,
    id: 0
  }
]
```

We can change our [`cy.visit()`](https://on.cypress.io/visit?ref=cypress-io.ghost.io) to pass us a `window` reference _before_ the application loads. That’s a great opportunity to set the initial state.

```js
it('can set initial todos', () => {
  cy.visit('/', {
    onBeforeLoad: win => {
      win.initialState = [
        {
          id: 0,
          text: 'first',
          completed: true
        },
        {
          id: 1,
          text: 'second',
          completed: false
        },
        {
          id: 2,
          text: 'third',
          completed: true
        }
      ]
    }
  })
  // there should be 3 items in the UI
  cy.get('.todo-list li').should('have.length', 3)
  // and 2 of them should be completed
  cy.get('.todo-list li.completed').should('have.length', 2)
})
```

Again, inlining a complex state object in each test hurts test readability. We can place it into a [fixture](https://on.cypress.io/fixture?ref=cypress-io.ghost.io) JSON file instead, for example into `cypress/fixtures/3-todos.json`

```json
[
  {
    "id": 0,
    "text": "first",
    "completed": true
  },
  {
    "id": 1,
    "text": "second",
    "completed": false
  },
  {
    "id": 2,
    "text": "third",
    "completed": true
  }
]
```

In the test we can use the [`cy.fixture()`](https://on.cypress.io/fixture?ref=cypress-io.ghost.io) command to load this JSON file.

```js
it('can set initial todos from a fixture', () => {
  cy.fixture('3-todos').then(todos => {
    cy.visit('/', {
      onBeforeLoad: win => {
        win.initialState = todos
      }
    })
  })
  // there should be 3 items in the UI
  cy.get('.todo-list li').should('have.length', 3)
  // and 2 of them should be completed
  cy.get('.todo-list li.completed').should('have.length', 2)
})
```

Again, this is just JavaScript, and if we expect a lot of tests to set different initial states, we should factor it out into a separate utility function.

```js
const initialVisit = (url, fixture) => {
  cy.fixture(fixture).then(todos => {
    cy.visit(url, {
      onBeforeLoad: win => {
        win.initialState = todos
      }
    })
  })
}

it('can set initial todos from a fixture', () => {
  initialVisit('/', '3-todos')
  // there should be 3 items in the UI
  cy.get('.todo-list li').should('have.length', 3)
  // and 2 of them should be completed
  cy.get('.todo-list li.completed').should('have.length', 2)
})
```

Works the same way.

## Snapshot testing

I have described in the blog post [End-to-End Snapshot Testing](https://www.cypress.io/blog/2018/01/16/end-to-end-snapshot-testing/?ref=cypress-io.ghost.io) my take on snapshot testing. But for this post I recommend trying out the [meinaart/cypress-plugin-snapshots](https://github.com/meinaart/cypress-plugin-snapshots?ref=cypress-io.ghost.io) plugin instead.

I have installed this plugin from my terminal.

```shell
npm i -D cypress-plugin-snapshots
```

Then I load the plugin within the `cypress/plugins/index.js` file…

```js
const { initPlugin } = require('cypress-plugin-snapshots/plugin')
module.exports = (on, config) => {
  initPlugin(on, config)
  return config
}
```

…and add a custom command in `cypress/support/index.js`

```js
import 'cypress-plugin-snapshots/commands'
```

I also need to create at least an empty configuration in `cypress.json` file.

```json
{
  "baseUrl": "http://localhost:3000",
  "ignoreTestFiles": [
    "**/*.snap",
    "**/__snapshot__/*"
  ],
  "env": {
    "cypress-plugin-snapshots": {}
  }
}
```

Here is my test - drive the app via the DOM and then expect the Redux store to match the snapshot:

```js
it('snapshots', () => {
  cy.visit('/')
  cy.focused().type('first{enter}').type('second{enter}')
  cy.contains('.todo-list li', 'second').find('input[type=checkbox]').click()
  cy.contains('.filters a', 'Completed').click()
  cy.window().its('store').invoke('getState').toMatchSnapshot()
})
```

The very first time the test runs, the snapshot is saved into the file `cypress/integration/__snapshots__/spec.js.snap` and it contains the state object.

```json
{
  "snapshots #0": {
    "todos": [
      {
        "completed": false,
        "id": 0,
        "text": "Use Redux"
      },
      {
        "completed": false,
        "id": 1,
        "text": "first"
      },
      {
        "completed": true,
        "id": 2,
        "text": "second"
      }
    ],
    "visibilityFilter": "show_completed"
  }
}
```

If the test runs again, the state object is compared against the saved object. Usually the snapshot matches, and the test passes.

But what happens if the Redux store has a different value for some reason? Let us change the test and find out. For example, let us add another todo item with the text “third”.

```js
it('snapshots', () => {
  cy.visit('/')
  cy.focused().type('first{enter}').type('second{enter}').type('third{enter}')
  cy.contains('.todo-list li', 'second').find('input[type=checkbox]').click()
  cy.contains('.filters a', 'Completed').click()
  cy.window().its('store').invoke('getState').toMatchSnapshot()
})
```

No worries, click the “Compare Snapshot” error box. A modal pops up that shows the difference between the previously saved snapshot and the current value.

Since we have modified the test and the new value is the new “gold” value, click the “Update Snapshot” button and the new value will be written into the snapshot file. All green now, bravo [Meinaart van Straalen](https://github.com/meinaart?ref=cypress-io.ghost.io) for the excellent developer experience!

## See also

[Testing Vue web applications with Vuex data store & REST backend](https://www.cypress.io/blog/2017/11/28/testing-vue-web-application-with-vuex-data-store-and-rest-backend/?ref=cypress-io.ghost.io)

Plugins: [cypress-pipe](https://github.com/NicholasBoll/cypress-pipe?ref=cypress-io.ghost.io), [cypress-plugin-snapshots](https://github.com/meinaart/cypress-plugin-snapshots?ref=cypress-io.ghost.io)

“End-to-end Testing Is Hard - But It Doesn’t Have to Be” ReactiveConf 2018 presentation [video](https://www.youtube.com/watch?v=swpz0H0u13k&ref=cypress-io.ghost.io), [slides](https://slides.com/bahmutov/reactive-conf?ref=cypress-io.ghost.io)