Vue.js 3 Unit Testing Vuex Tutorial

In this Vue tutorial we learn how to test Vuex state management.

We cover testing a local store, mutations, data persistence and getters.

Lesson Video

If you prefer to learn visually, you can watch this lesson in video format.

Lesson Project

If you want to follow along with the examples, you will need an app generated by the Vue CLI that includes Jest as well as Vuex .

The project should look similar to the following.

Example: project
project-name/
├── public/
├── src/
|   ├── store/
|   |   └── index.js
|
├── tests/
|   ├── unit/
|   |   └── example.spec.js
|
└── jest.config.js

To keep things simple we’ll use the example.spec.js file to write our tests in, so it’s safe to delete the code in it.

How to test Vuex

Vuex helps us manage state across our application by synchronizing data among components.

We do so by creating a store that holds the entire application’s state.

Example: Vuex store
import { createApp   } from 'vue'
import { createStore } from 'vuex'
import App from './App.vue'

const store = createStore({
  state() {
    return {
      property: value
    }
  },
  mutations: {
    mutate(state) {
      // perform operation on
      // state.property
    }
  }
})

const app = createApp(App)

app.use(store)
app.mount('#app')

Then, we can interact with the store by using the special $store instance variable. For example, committing a mutation.

Example: store interaction
<template>
  <button @click="someMutation">Mutate in some way</button>
</template>

<script>
export default {
  methods: {
    someMutation() {
      this.$store.commit('mutate')
    }
  }
}
</script>

When unit testing Vuex, we want to test the following.

  • Mutations
  • Actions
  • Getters

How to test Vuex mutations

When testing mutations, we should check that a state object was mutated correctly. It’s after all the purpose of a mutation.

They’re simple to test because they’re regular Javascript functions and just need to be invoked for us to test the effect. We don’t invoke them directly though, we commit a mutation and Vue will invoke the function behind the scenes.

Example: test mutation
// import store
import store from 'path-to-store'

describe('Vuex', () => {
  it('test-summary', () => {

    // commit mutation
    store.commit('mutation_function')

    // test its effect
    expect(store.state.value).toBe(value)
  })
})

As an example, we’ll use an increment mutation in our store that increases a counter by 1 when it’s invoked.

Example: src/store/index.js
import { createStore } from 'vuex'

export default createStore({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++
    }
  }
})

In the test, we’ll commit the increment mutation and test if count incremented from 0 to 1.

Example: tests/unit/example.spec.js
// import store
import store from '@/store/index'

describe('Vuex', () => {
  it('mutation increments count', () => {

    // commit mutation
    store.commit('increment')

    // test its effect
    expect(store.state.count).toBe(1)
  })
})

If we run the test, it should pass with the following output.

Output:
PASS  tests/unit/example.spec.js
 Vuex
   √ mutation increments count (4ms)

Data persistence between tests

It’s important to remember that we’re working with saved state. Store data will be persistent between tests.

As an example, let’s add a “decrement” mutation to our store that decrements count by 1.

Example: src/store/index.js
import { createStore } from 'vuex'

export default createStore({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++
    },
    decrement(state) {
      state.count--
    }
  }
})

We’ll add another it block to our test and check if count decrements from 0 to -1.

Example: tests/unit/example.spec.js
import store from '@/store/index'

describe('Vuex', () => {
  it('increment count from 0 to 1', () => {

    store.commit('increment')
    expect(store.state.count).toBe(1)
  }),
  it('decrement count from 0 to -1', () => {

    store.commit('decrement')
    expect(store.state.count).toBe(-1)
  })
})

When we run the test, it will fail with the following output.

Output: tests/unit/example.spec.js
FAIL  tests/unit/example.spec.js
 Vuex
   √ increment count from 0 to 1 (3ms)
   × decrement count from 0 to -1 (4ms)

 ● Vuex › decrement count from 0 to -1

   expect(received).toBe(expected) // Object.is equality

   Expected: -1
   Received: 0

     10 |
     11 |     store.commit('decrement')
   > 12 |     expect(store.state.count).toBe(-1)
        |                               ^
     13 |   })
     14 | })
     15 |

     at Object.<anonymous> (tests/unit/example.spec.js:12:31)

From the output we can see that Jest expected -1, but received 0.

That’s because count ’s state was saved after the first test. So the second test started to decrement from 1 instead of 0.

In some situations it may be the behavior we want. If it’s not however, a simple solution is to reset any test data before each test with the beforeEach utility method.

Example: tests/unit/example.spec.js
import store from '@/store/index'

describe('Vuex', () => {

  // reset any values that are
  // affected by the mutation
  // we're currently testing
  beforeEach(() => {
    store.state.count = 0;
  })

  it('increment count from 0 to 1', () => {

    store.commit('increment')
    expect(store.state.count).toBe(1)
  }),
  it('decrement count from 0 to -1', () => {

    store.commit('decrement')
    expect(store.state.count).toBe(-1)
  })
})

This time the test will pass.

Output:
PASS  tests/unit/example.spec.js
 Vuex
   √ increment count from 0 to 1 (4ms)
   √ decrement count from 0 to -1

How to test Vuex mutations with a local store

From the previous section we saw how data persists between single tests. Another problem with using a real store is that if we import it into a file that contains multiple tests, the data will be shared by all the tests.

This will cause the same problem as above where tests end up with unexpected values. A solution to this problem is to create a local store for each test that contains only what we want to test.

We create our local store inside the test block with the createStore method from the ‘vuex’ package. We don’t need to recreate the whole store, only the parts we want to test.

As an example, let’s create a local store that stores the count state and increments it with the increment mutation.

Example: tests/unit/example.spec.js
import { createStore } from 'vuex'

describe('Vuex', () => {
  it('mutation increments count', () => {

    // create the part of the
    // store we want to test
    const store = createStore({
      state: {
        count: 0
      },
      mutations: {
        increment(state) {
          state.count += 1
        }
      }
    })
  })
})

From there, everything is the same as when we used the real store. This time we commit the local store’s mutation and test its effect.

Example: tests/unit/example.spec.js
import { createStore } from 'vuex'

describe('Vuex', () => {
  it('mutation increments count', () => {

    // create the part of the
    // store we want to test
    const store = createStore({
      state: {
        count: 0
      },
      mutations: {
        increment(state) {
          state.count += 1
        }
      }
    })

    // commit mutation on
    // our local store
    store.commit('increment')

    // test its effect
    expect(store.state.count).toBe(1)
  })
})

If we run the test, it should pass with the following output.

Output:
PASS  tests/unit/example.spec.js
 Vuex
   √ mutation increments count (5ms)

How to test Vuex getters

As we learned in the Vuex lesson , we don’t access a state data property directly. Instead, we use a getter function to return the value of that property.

A getter just returns a value, so we test that value before and/or after it has been mutated.

We access a getter through the getters property on an imported or local store, then chain the name of the getter we want to it.

Example: access getter
store.getter.function_name

As an example for the imported store, we’ll add a getter function to the store that returns count .

Example: src/store/index.js
import { createStore } from 'vuex'

export default createStore({
  state: {
    count: 0
  },
  getters: {
    getCount(state) {
      return state.count
    }
  },
  mutations: {
    increment(state) {
      state.count++
    }
  }
})

Then we use store.getters.getCount instead of store.state.count in the test.

Example: tests/unit/example.spec.js
import store from '@/store/index'

describe('Vuex', () => {
  it('increment count from 0 to 1', () => {

    store.commit('increment')
    expect(store.getters.getCount).toBe(1)
  })
})

If we run the test, it should pass with the following output.

Output:
PASS  tests/unit/example.spec.js
 Vuex
   √ increment count from 0 to 1 (4ms)

As an example for the local store we’ll use the exact same store code, but create it inside the it block in the test file.

Example: tests/unit/example.spec.js
import { createStore } from 'vuex'

describe('Vuex', () => {
  it('increment count from 0 to 1', () => {

    // create the part of the
    // store we want to test
    const store = createStore({
      state: {
        count: 0
      },
      getters: {
        getCount(state) {
          return state.count
        }
      },
      mutations: {
        increment(state) {
          state.count++
        }
      }
    })

    // commit mutation on
    // our local store
    store.commit('increment')

    // test its effect
    expect(store.state.count).toBe(1)
  })
})

The test is the same except for the fact that it uses a local store instance instead of the real one. The test should pass with the following output.

Output:
PASS  tests/unit/example.spec.js
 Vuex
   √ increment count from 0 to 1 (5ms)

Further Reading

For more information on the topics covered in this lesson, please see the relevant sections below.