Unit testing Vuex data store using Cypress.io Test Runner
Tags: testing, cypress
Published: 2019-12-22 18:13:18

---
title: Unit testing Vuex data store using Cypress.io Test Runner
published: true
description: Cypress End-to-End test runner can also run unit tests. This example shows easy testing of a Vuex datastore code
tags: testing, cypress
---

Cypress [Test Runner](http://github.com/cypress-io/cypress) has become a [very popular tool](https://2019.stateofjs.com/testing/) for writing end-to-end tests, but did you know it can also run unit tests in a real browser? This post shows how to unit test your typical front end code, like the Vuex data store. The post largely follows the example from official [Vuex testing page](https://vuex.vuejs.org/guide/testing.html), and you can find all source code in the [bahmutov/test-vuex-with-cypress](https://github.com/bahmutov/test-vuex-with-cypress) repo.

## Setup

For our example, all we need are Vue and Vuex libraries and Cypress. Install them using NPM commands

```shell
npm install --save vue vuex
npm install --save-dev cypress
```

or Yarn commands

```shell
yarn add vue vuex
yarn add -D cypress
```

Cypress includes Mocha test runner, Electron browser, Chai assertions, and many other tools - we should be good to go without installing anything else.


## Testing mutations

The first example I would like to test is a counter that starts at zero and increments the state via a mutation. Here is source file `src/counter.js`

```js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const state = {
  count: 0,
}
export const mutations = {
  increment(state) {
    state.count++
  },
}

export default new Vuex.Store({
  state,
  mutations,
})
```
My spec (test) file will reside in `cypress/integration/counter-spec.js` and uses the familiar BDD syntax.

```js
import { mutations } from '../../src/counter'

describe('mutations', () => {
  context('increment', () => {
    const { increment } = mutations
    it('INCREMENT', () => {
      const state = { count: 0 }
      increment(state)
      // see https://on.cypress.io/assertions
      // for "expect" examples
      expect(state.count).to.equal(1)
    })
  })
})
```

The source files are structured like this:

```text
test-vuex-with-cypress/
  package.json
  src/
    counter.js
  cypress/
    integration/
      counter-spec.js
  cypress.json
```

Open Cypress with `npx cypress open` or `yarn cypress open` command and click on `counter-spec.js`. 

![Selecting spec file to run](https://thepracticaldev.s3.amazonaws.com/i/1zoozz2zipvfgosm98ab.png)

The test should pass.

![First passing unit test](https://thepracticaldev.s3.amazonaws.com/i/kgkb5ma23o6gco1qfudh.png)

Cypress watches all loaded files - and it automatically reruns the tests when a file changes. In the screen recording below I have my text editor on the left and Cypress on the right. As I keep coding and saving, I am watching the tests pass and fail - a true test-driven development experience.

![Running unit tests using Cypress](https://thepracticaldev.s3.amazonaws.com/i/kuuvuwqohwtzv1ff6tzs.gif)

The iframe on the right stays empty - because our test does not visit a website. But we can take the full advantage of the Command Log column on the left side of the Test Runner. Currently, it is showing a single passing assertion. Let's change our assertions slightly - I will use [`cy.wrap`](https://on.cypress.io) command to wrap the state object and yield it to the `increment` function, and then check the value of the property `count` using `should(...)` [BDD assertion](https://on.cypress.io/assertions#BDD-Assertions).

```js
import { mutations } from '../../src/counter'

describe('mutations', () => {
  context('increment', () => {
    const { increment } = mutations
    it('increments the state', () => {
      // wrapped state object will be passed
      // to the "increment" callback
      // which will return new object
      cy.wrap({ count: 0 })
        .then(increment)
        // and the next assertion will run against
        // the updated state
        .should('have.property', 'count', 1)
    })
  })
})
```
The test passes - and because Cypress "knows" about the wrapped value, it can show it in the Command Log.

![Wrapping state using cy.wrap](https://thepracticaldev.s3.amazonaws.com/i/q51tppmfpyv147akirnc.png)

Nice. The assertions `expect(...).to.equal` and `wrap(...).should('equal')` are equivalent, and everything depends on what you find more readable and useful in the GUI.

If our mutation expects a payload after the `state` argument like

```js
export const mutations = {
  increment(state, n = 1) {
    state.count += n
  },
}
```
We can pass it explicitly

```js
it('increments by 5', () => {
  cy.wrap({ count: 0 })
    .then(state => increment(state, 5))
    .should('deep.equal', { count: 5 })
})
```

## Testing store

Let's try testing the entire store. 

```js
import store from '../../src/counter'
describe('store commits', () => {
  it('starts with zero', () => {
    // it is a good idea to add assertion message
    // as the second argument to "expect(value, ...)"
    expect(store.state.count, 'initial value').to.equal(0)
    store.commit('increment', 4)
    expect(store.state.count, 'changed value').to.equal(4)
  })
})
```

As we write more test we quickly discover one potential landmine - the imported `store` object is a singleton!

```js
describe('store commits', () => {
  it('starts with zero', () => {
    // it is a good idea to add assertion message
    // as the second argument to "expect(value, ...)"
    expect(store.state.count, 'initial value').to.equal(0)
    store.commit('increment', 4)
    expect(store.state.count, 'changed value').to.equal(4)
  })

  it('starts with zero again', () => {
    expect(store.state.count, 'initial value').to.equal(0)
  })
})
```
![Second test starts with state left by the first test](https://thepracticaldev.s3.amazonaws.com/i/hzyogvd7ancboaq0sf1k.png)

At Cypress we [strongly believe](https://on.cypress.io/best-practices) that every test should be independent of any other test; that each test should set its own initial data. To avoid singletons we can change our Counter code to return a factory function instead of the store object. Every call to the factory function should return a brand new store, with its own separate `state` object.

```js
// src/counter.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export const mutations = {
  increment(state, n = 1) {
    state.count += n
  },
}

const counterStoreFactory = () => {
  const state = {
    count: 0,
  }
  return new Vuex.Store({
    state,
    mutations,
  })
}
export default counterStoreFactory
```

Before each test, we create the store object in a local closure

```js
import storeFactory from '../../src/counter'
describe('store commits', () => {
  let store

  beforeEach(() => {
    store = storeFactory()
  })

  it('starts with zero', () => {
    // it is a good idea to add assertion message
    // as the second argument to "expect(value, ...)"
    expect(store.state.count, 'initial value').to.equal(0)
    store.commit('increment', 4)
    expect(store.state.count, 'changed value').to.equal(4)
  })

  it('starts with zero again', () => {
    expect(store.state.count, 'initial value').to.equal(0)
  })
})
```

Now the tests pass, no matter in what order they run, if any tests are skipped with `it.skip`, or if an individual test is enabled exclusively using `it.only` modifier.

![Both tests are passing now](https://thepracticaldev.s3.amazonaws.com/i/uhsq3wgcq1tu7pb55awg.png)

## Time-traveling debugger

When Cypress runs the commands and assertions it links the objects it operates on to each command. For example, let's assert the entire state object in the next test.

```js
it('compares entire state object', () => {
  expect(store.state).to.deep.equal({
    count: 0,
  })
  // equivalent assertion
  cy.wrap(store)
    .its('state')
    .should('deep.equal', {
      count: 0,
    })
})
```

When the test passes, we can click on the `wrap` or `its` commands shown in the Command Log on the left. Then if the DevTools console is open, the Test Runner prints the arguments as a real object that you can inspect further.

![Inspecting the state object](https://thepracticaldev.s3.amazonaws.com/i/ay7o9n2c0bf9xx9g951r.png)

One has to be cautious when inspecting objects though - the reference points at the _real_ object that can change later. For example, the next test checks the initial `count` value, then calls the increment commit mutation, and then the test checks the updated state. 

```js
it('saves objects like checkpoints', () => {
  cy.wrap(store)
    .its('state')
    .should('deep.equal', {
      count: 0,
    })
  cy.wrap(store).invoke('commit', 'increment', 10)
  cy.wrap(store)
    .its('state')
    .should('deep.equal', {
      count: 10,
    })
})
```

The test passes, but when we inspect the _first_ state value, the object shows "count: 10".

![State object shows 10 even if the assertion confirmed it was zero](https://thepracticaldev.s3.amazonaws.com/i/vz90pxik9oi3ae0g248q.png)

We know the `count` was zero because the first assertion has passed, but when we inspect the value we see the current value. This is similar to the way `console.log(object)` shows the latest value in the DevTools console as the next gif demonstrates.

![console.log shows the updated value](https://thepracticaldev.s3.amazonaws.com/i/067cfqhd9ganyizl6cr2.gif)

The trick I like to do to avoid any confusion is to make a clone of the state object before comparing it.

```js
it('saves objects like checkpoints', () => {
  cy.wrap(store)
    .its('state')
    .then(Cypress._.cloneDeep)
    .should('deep.equal', {
      count: 0,
    })
  cy.wrap(store).invoke('commit', 'increment', 10)
  cy.wrap(store)
    .its('state')
    .then(Cypress._.cloneDeep)
    .should('deep.equal', {
      count: 10,
    })
})
```

Cypress includes [Lodash library](https://on.cypress.io/bundled-tools) which lets us call [_.cloneDeep](https://lodash.com/docs/4.17.15#cloneDeep) function. This has the additional benefit of showing plain objects because we no longer deal with Vue's reactive getters and setters.

![State clones and assertions](https://thepracticaldev.s3.amazonaws.com/i/sua179nopr7lneor7co9.png)

To see the full state object clone, click on the assertion message in the Command Log. The full value as it was during that instant of the test will be dumped to the DevTools console.

## Asynchronous actions

Vuex store allows performing asynchronous updates via [actions mechanism](https://vuex.vuejs.org/guide/actions.html). Let's add an action to increment the count after one second delay.

```js
// same mutations as before

const actions = {
  incrementAsync({ commit }, n = 1) {
    setTimeout(() => {
      commit('increment', n)
    }, 1000)
  },
}

const counterStoreFactory = () => {
  const state = {
    count: 0,
  }

  return new Vuex.Store({
    state,
    mutations,
    actions,
  })
}
export default counterStoreFactory
```

We cannot test the count update synchronously. The following test fails:

```js
it('can be async (fails)', () => {
  store.dispatch('incrementAsync', 2)
  // the next assertion will fail immediately
  expect(store.state.count).to.equal(2)
})
```

![Test fails](https://thepracticaldev.s3.amazonaws.com/i/owk2amfjpi88omsdircp.png)

Typically, to test something like this, the test runner would need to be aware of the framework's details to know when the state commit has happened. But Cypress has a better framework-independent way of dealing with asynchronous operations. It has a built-in [retry-ability](https://on.cypress.io/retry-ability). In essence, an idempotent command will be retried until the assertions that immediately follow it pass or the command times out. Let's rewrite the test to retry.

```js
it('can be async (passes)', () => {
  store.dispatch('incrementAsync', 2)
  cy.wrap(store.state)  // command
    .its('count')       // command
    .should('equal', 2) // assertion
})
```

The command `its('count')` is followed by assertion `should('equal', 2)`. Initially, the count is zero, and the assertion fails. Cypress then runs `its('count')` again, passing the value to the assertion. It does this repeatedly until the action `incrementAsync` finishes and commits updated `count`. The command `its('count')` yields 2, the assertion `should('equal', 2)` passes and the test completes.

![Test retries until async action does its job](https://thepracticaldev.s3.amazonaws.com/i/i5nrjb1xp5uru4jv4ve2.gif)

**Tip:** you can write pretty complex asynchronous tests by waiting for your own predicates using [cypress-wait-until](https://github.com/NoriSte/cypress-wait-until) plugin.

## Spying and stubbing

During testing we might want to assert that the store's methods were called (by spying on them) or even change their behavior (using stubs). 

Cypress Test Runner comes with [Sinon.js](https://sinonjs.org/) library [bundled](https://on.cypress.io/bundled-tools), let's use it. Because Vuex is implemented as a series of namespaced modules, we cannot spy on the method `store.commit`, instead, we need to spy on the `store._modules.root.context` object that has the actual `commit` method.

```js
it('calls commit "increment" in the store', () => {
  cy.spy(store._modules.root.context, 'commit').as('commit')
  store.dispatch('incrementAsync', 2)
  cy.wrap(store.state)
    .its('count')
    .should('equal', 2)
  // we can also assert directly on the spies
  // thanks for Sinon-Chai bundled with Cypress
  // https://on.cypress.io/assertions#Sinon-Chai
  cy.get('@commit').should('have.been.calledOnce')
})
```

The test passes, and the "Spies / Stubs" table shows our spy under alias "commit" and when it was called during the test.

![Spy was called](https://thepracticaldev.s3.amazonaws.com/i/lsjlvr5arscyqwlpxyzs.png)

Sinon spies in Cypress play nicely with retry-ability. We don't have to rely on the state assertion to "wait" until the assertion passes.

```js
it('spy retries assertion', () => {
  cy.spy(store._modules.root.context, 'commit').as('commit')
  store.dispatch('incrementAsync', 2)  
  store.dispatch('incrementAsync', 5)
  cy.get('@commit').should('have.been.calledTwice')
})
```
![Waiting on the spy to be called twice](https://thepracticaldev.s3.amazonaws.com/i/d0usp8m5jbbn5qsd73eu.gif)

We can make the spy more precise, for example by only spying on calls made with specific arguments.

```js
it('spies on specific call', () => {
  cy.spy(store._modules.root.context, 'commit')
    .withArgs('increment', 5)
    .as('commit5')
  store.dispatch('incrementAsync', 2)
  store.dispatch('incrementAsync', 5)
  cy.get('@commit5').should('have.been.calledOnce')
})
```

![Spying only on commit to increment by 5](https://thepracticaldev.s3.amazonaws.com/i/wfenru07y40qd99kl45b.gif)

Every call to the spy is recorded, and you can click on its record to see more details in the DevTools console.

![Details of the commit to increment by 5 call](https://thepracticaldev.s3.amazonaws.com/i/5gs7vxphouax3ucmgcwu.png)

Spying is a nice way to confirm the store receives the expected calls. Let's add method stubbing where we can change the behavior of actions and mutations. In the test below we will allow all mutations to go through unchanged, but whenever there is mutation to increment count by 5, we will change the call and will increment by 100 instead.

```js
it('stubs increment commit', () => {
  // allow all mutations to go through
  // but ("increment", 5) will call our fake function
  cy.stub(store._modules.root.context, 'commit')
    .callThrough()
    .withArgs('increment', 5)
    .callsFake((name, n) => {
      // confirm we are only stubbing increments by 5
      expect(n).to.equal(5)
      // call the original method, but pass a different value
      store._modules.root.context.commit.wrappedMethod(name, 100)
    })

  store.dispatch('incrementAsync', 2)
  store.dispatch('incrementAsync', 5)

  // our stub will turn increment by 5 to increment by 100 😃
  cy.wrap(store.state)
    .its('count')
    .should('equal', 102)
})
```
![Stubbed method test](https://thepracticaldev.s3.amazonaws.com/i/nmlq6x17b3g57lz9v8a3.gif)

To explore spying further, read the Cypress [Spies, Stubs And Clocks](https://on.cypress.io/stubs-spies-and-clocks) guide.

**Tip:** you can simplify accessing commit context and created spy using test context object and local variables.

```js
beforeEach(function() {
  store = storeFactory()
  // save Vuex context in the test's own object
  this.context = store._modules.root.context
})
it('grabs context', function() {
  // save created spy in local variable
  const commit = cy.spy(this.context, 'commit')
  store.dispatch('incrementAsync', 2)
  // use Cypress retry-ability to wait for
  // the spy to be called once
  cy.wrap(commit).should('have.been.calledOnce')
})
```
![Local variable to access the spy](https://thepracticaldev.s3.amazonaws.com/i/h8grf2hfyd1mdzhuq79z.gif)

Alternatively, give the spy an alias to access it - it will give more context to the Command Log.

```js
it('uses alias', function() {
  // save created spy as an alias
  cy.spy(this.context, 'commit').as('commit')
  store.dispatch('incrementAsync', 2)
  // use Cypress retry-ability to wait for
  // the spy to be called once
  cy.get('@commit').should('have.been.calledOnce')
})
```
![Spy alias](https://thepracticaldev.s3.amazonaws.com/i/bosypxq7vqqb9h5whhlx.png)

**Tip:** Cypress automatically removes all spies and stubs before each test, so you don't have to clean them up.

## Code coverage

Let's make sure we are covering our code with unit tests. I will use [Cypress code coverage plugin](https://github.com/cypress-io/code-coverage) and `istanbul` to instrument unit tests as described in Cypress [code coverage guide](https://on.cypress.io/code-coverage). Install the plugin and instrumentation peer dependencies:

```shell
npm install --save-dev @cypress/code-coverage \
  nyc istanbul-lib-coverage babel-plugin-istanbul
+ istanbul-lib-coverage@2.0.5
+ nyc@14.1.1
+ @cypress/code-coverage@1.10.4
+ babel-plugin-istanbul@5.2.0
```

Then add the following line to `cypress/support/index.js` file

```js
import '@cypress/code-coverage/support'
```

and the following lines to `cypress/plugins/index.js` file

```js
module.exports = (on, config) => {
  on('task', require('@cypress/code-coverage/task'))
  on(
    'file:preprocessor',
    require('@cypress/code-coverage/use-browserify-istanbul'),
  )
}
```

Run unit tests again - the code coverage will be automatically merged and saved in several formats in the folder `coverage`

```text
coverage/
  clover.xml 
  coverage-final.json
  lcov-report/
    index.html
  lcof.info
```

3rd party coverage services accept `lcov.info` and `clover.xml`, while for me the static page report in `lcov-report/index.html` is the most useful - it is a guide showing which parts of the code were hit by the tests. The counter tests were very thorough - they have covered all statements in the code, except the default parameter value (marked in yellow).

```shell
open coverage/lcov-report/index.html
```
![Code coverage](https://thepracticaldev.s3.amazonaws.com/i/nrhbr0c00t1qnqbt42ez.png)


Add one more unit test - and get the complete code coverage!

```js
it('increments async by default value', () => {
  store.dispatch('incrementAsync')
  cy.wrap(store.state)
    .its('count')
    .should('equal', 1)
})
```

## Bonus: run tests using GitHub Action

Testing locally is important, but testing every commit pushed to the repository is even more valuable. Recently, GitHub Actions became generally available. I looked into GH Actions in the blog post [Trying GitHub Actions](https://glebbahmutov.com/blog/trying-github-actions/) and came very impressed. To make using Cypress from a GH Action simpler, we have published our official custom action called [cypress-io/github-action](https://github.com/cypress-io/github-action). Here is how to set up testing on each code push:

First, create file `.github/workflows/main.yml` and paste the following example from [cypress-io/github-action README](https://github.com/cypress-io/github-action#readme).

```yml
name: tests
on: [push]
jobs:
  cypress-run:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v1
      # Install NPM dependencies, cache them correctly
      # and run all Cypress tests
      - name: Cypress run
        uses: cypress-io/github-action@v1
```

Second, push a commit to the repository. 

Third - there is no third step! The tests will be running already, you can see them at [bahmutov/test-vuex-with-cypress/actions](https://github.com/bahmutov/test-vuex-with-cypress/actions). NPM installation, caching, running Cypress command - it is all done by the custom action `cypress-io/github-action@v1` and the default values should handle our unit tests without any additional parameters.

![Passing tests in actions](https://thepracticaldev.s3.amazonaws.com/i/9nh7jphet2f8hc8gqvcc.png)

I have also added a badge to the README to know the status of the `master` branch. Here is the Markdown syntax:

```text
https://github.com/<OWNER>/<REPOSITORY>/workflows/<WORKFLOW_NAME>/badge.svg?branch=<BRANCH>

![test status](https://github.com/bahmutov/test-vuex-with-cypress/workflows/tests/badge.svg?branch=master)
```

## Conclusions

I am part of the Cypress team, so I am partial (pun intended) to this Test Runner. I like the GUI, the file watching, the time-traveling debugger, the built-in assertions and function spies and stubs. I think running front-end unit tests inside a real browser is the preferred way, and the ability to use DevTools to inspect everything the tests do gives one superpower. Take Cypress for a spin - it is not just an end-to-end test runner!

Find the full source code for this blog post at [bahmutov/test-vuex-with-cypress](https://github.com/bahmutov/test-vuex-with-cypress) repo. If you want to see more unit testing with Cypress examples, check out our [unit testing recipes](https://github.com/cypress-io/cypress-example-recipes#unit-testing).