DEV Community

loading...

Vue TDD by example episode 2: add vuex store

Jur de Vries
I am an independent software engineer and musician/artist. That makes me a coding artist. I believe in the software craftsmanship movement.
Originally published at thecodingartist.com ・9 min read

In the previous episode we created a todo app using Test Driven Development. The idea is that it is easier to refactor code once there are unit tests. In this episode we will start refactoring the previously build app.

The changed requirement

We created a very simple todo component. Now suppose we have a big Vue application in which the todo functionality is only one of the functionalities. Apart from that: in the future we want the todos to be visible from anywhere in the app, but the add-form only on a dedicated place. What to do? First thing is that we need a central location for the state. We will solve this by adding Vuex to the mix and creating a store module for the todos.

We will start where the previous tutorial ended. You can find the code to start with on github

The plan for adding the store

When we move the data storage from the components state to a vuex store module, the existing public interface should not change: the end user should not see any difference in the component.
Getting data from and mutating data in the store however should be regarded as the public interface of a component as well, so that would mean we should write unit tests for these interactions.
Let's defer the writing of these tests for now. Because the idea of tdd is to do small steps, we will decide later if and how we will test the integration of the store in the todo component.
What we will do first is:

  1. Create a todo store using TDD
  2. Change the implementation of the todo component, so it uses the store instead of local data, without adding tests to the existing test suite.

Step 0: Add Vuex

In order to use the vuex store, we have to add it to the project. This is a simple procedure using the vue cli:

vue add vuex

After having performed this command, there should be an empty store in your project and it should have been added in main.js.

The code can be found on github

Step 1: create the todo module

First we need a test which forces us to write the code for the store. Let's start with the simplest of all items in a store: the state. We will use a function to create the initial state (an explanation about why to use a function can be found at the vuex documentation).

Let's create our first failing test:

// tests/unit/store/todo.spec.js

import todo from '@/store/todo.js'

describe('The todo store', () => {
  it('uses a function to generate the initial state', () => {
    const newState = todo.state()
    expect(newState).not.toBeUndefined()
  })
})

The first error is that the file could not be found, as expected. Let's add the file with a default export containing the whole store module:

// src/store/todo.js

export default {
  state: () => {}
}

After checking that this really fails, we make the test pass by simply returning an empty object:

// src/store/todo.js

export default {
  state: () => {
    return {}
  }
}

Okay, on to the next test. Let's define the datastructure:

  it('stores the todos at the todos key', () => {
    const newState = todo.state()
    expect(newState).toEqual({ todos: [] })
  })

And it fails. Let's make it pass:

export default {
  state: () => {
    return {
      todos: []
    }
  }
}

Now we have defined the state, let's add the mutation to add a todo using a nested suite which describes all the mutations:

  describe(', the mutations', () => {
    it('a todo can be added using the ADD_TODO mutation', () => {
      const state = todo.state()
      todo.mutations.ADD_TODO(state, 'A random todo description')
      expect(state).toEqual({
        todos: [{
          description: 'A random todo description',
          done: false
        }]
      })
    })
  })

And implement it (we will skip the adding of a function that does nothing to speed up the process):

  mutations: {
    ADD_TODO (state) {
      state.todos.push({
        description: 'A random todo description',
        done: false
      })
    }
  }

Now let's add a second test to force us to really use the description:

    it('a todo can be added using the ADD_TODO mutation passing a description', () => {
      const state = todo.state()
      todo.mutations.ADD_TODO(state, 'Another random todo description')
      expect(state).toEqual({
        todos: [{
          description: 'Another random todo description',
          done: false
        }]
      })
    })

And we can get this to pass using:

  mutations: {
    ADD_TODO (state, description) {
      state.todos.push({
        description,
        done: false
      })
    }
  }

You might ask: do we really need to add all these tiny steps? The answer is: 'no, not always'. In many cases you can work more coarse-grained but remember you can always get back to the simple fine-grained steps if the solutions you want to create is hard to understand. For the rest of the tutorial I will omit the fine-grained steps.

Now we want to add more than one todo and assure that the order in which they are entered will be preserved:

    it('the order in which the todos are added are preserved in the state', () => {
      const state = todo.state()
      todo.mutations.ADD_TODO(state, 'First todo')
      todo.mutations.ADD_TODO(state, 'Second todo')
      expect(state).toEqual({
        todos: [
          {
            description: 'First todo',
            done: false
          },
          {
            description: 'Second todo',
            done: false
          }
        ]
      })
    })

This test passes, as we already expected. We want to be sure this is the case though, which is the reason we added it. Now we can start refactoring the tests, as one of the tests is redundant, and we can move the initialisation of the state to a beforeEach function. The test file looks like now:

import todo from '@/store/todo.js'

describe('The todo store', () => {
  it('stores the todos at the todos key', () => {
    const newState = todo.state()
    expect(newState).toEqual({ todos: [] })
  })

  describe(', the mutations', () => {
    let state

    beforeEach(() => {
      state = todo.state()
    })
    it('a todo can be added using the ADD_TODO mutation', () => {
      todo.mutations.ADD_TODO(state, 'A random todo description')
      expect(state).toEqual({
        todos: [{
          description: 'A random todo description',
          done: false
        }]
      })
    })
    it('the order in which the todos are added are preserved in the state', () => {
      todo.mutations.ADD_TODO(state, 'First todo')
      todo.mutations.ADD_TODO(state, 'Second todo')
      expect(state).toEqual({
        todos: [
          {
            description: 'First todo',
            done: false
          },
          {
            description: 'Second todo',
            done: false
          }
        ]
      })
    })
  })
})

In order to finish the mutations, we also need to be able to toggle the status of a todo:

    it('has a mutation to toggle the status of a todo', () => {
      state = {
        todos: [
          {
            description: 'First todo',
            done: false
          },
          {
            description: 'Todo to toggle',
            done: false
          }
        ]
      }
      todo.mutations.TOGGLE_TODO(state, {
        description: 'Todo to toggle',
        done: false
      })
      expect(state.todos).toEqual([
        {
          description: 'First todo',
          done: false
        },
        {
          description: 'Todo to toggle',
          done: true
        }
      ])
    })

This can be implemented using:

    TOGGLE_TODO (state, targetTodo) {
      const todo = state.todos.find(item => item.description === targetTodo.description)
      if (todo) {
        todo.done = !todo.done
      }
    }

Note that we use the description of the todo as an id. It might be better to generate an id but for simplicity we stick to the description. Also, in almost all cases the todo we pass to the mutation will be the same object as the todo in the list, but we cannot rely on this. For this reason we perform a lookup of the todo item based on the description.

The only thing we have left for the store is a getter to get all the todos:

  describe('the getters', () => {
    const state = {
      todos: [
        {
          description: 'First todo',
          done: false
        },
        {
          description: 'Second todo',
          done: false
        }
      ]
    }
    const todos = todo.getters.todos(state)
    expect(todos).toEqual([
      {
        description: 'First todo',
        done: false
      },
      {
        description: 'Second todo',
        done: false
      }
    ])
  })

And lets fix this:

  getters: {
    todos (state) {
      return state.todos
    }
  }

We have all the tests, but they don't read well. That is an assignment for you: what would you do to make the test more readable? You can find my solution in the repo.

The code till now can be found on github

Aside: what is a unit in case of the vuex store?

When writing the last part it kind of struck me that in testing the store module the internal state of the module is exposed regularly in the tests. Although in the official documentation of the vuex store testing this is the suggested way, it feels to me like testing the implementation to much. What if we want to alter the way the information is stored?

In fact, we can ask ourselves: what is a unit? If you compare a store module to a class, you could argue that the store module is a kind of class in which the actions, mutations and getters are the public interface. If you follow this reasoning it would mean that you only test the store using the public interface, which would mean you would create a real vuex store first and start testing that.

I may elaborate more on this in a separate post, but for now I leave it as is.

Step 2: use the store in the component

Before we can even use the store in our component tests, we need to do two things:

  1. Add the todo module to the store
  2. Ensure we use the store in our test suite by using a local copy of vue.

Adding our new store module to the store is almost trivial:

// src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import todo from '@/store/todo'

Vue.use(Vuex)

export default new Vuex.Store({
  ...
  modules: {
    todo
  }
})

Note that we didn't namespace the module. This is intended.

In order to use the store in the test we need to create a local vue instance and indicate that this local vue instance uses our store:

import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import Todo from '@/components/Todo'
import store from '@/store'

const localVue = createLocalVue()
localVue.use(Vuex)


describe('The Todo.vue component', () => {
  ...
  // Add the localVue to the mounting options every time when a mount is done.  
  const wrapper = shallowMount(Todo, {
    localVue,
    store,
    propsData: {
      title: 'A random title'
    }
  })
  ...  
})

Now we can start refactoring the component so it uses the store instead of the local data. Note that we don't alter the tests yet! The diff looks like:

// src/components/Todo.vue

<script>
+import { mapGetters } from 'vuex'
+
 export default {
   name: 'Todo',

+  computed: {
+    ...mapGetters(['todos'])
+  },
+
   data () {
     return {
-      todos: [],
       newTodo: ''
     }
   },

   methods: {
     addTodo () {
-      this.todos.push({
-        description: this.newTodo,
-        done: false
-      })
+      this.$store.commit('ADD_TODO', this.newTodo)
       this.newTodo = ''
     },
     toggle (todo) {
-      todo.done = !todo.done
+      this.$store.commit('TOGGLE_TODO', todo)
     }
   }

And all tests pass except one:

 FAIL  tests/unit/components/Todo.spec.js
  ● The Todo.vue component › adding todo items › displays the items in the order they are entered

    expect(received).toMatch(expected)

    Expected substring: "First"
    Received string:    "Mark done
          My first todo item"

      65 |       await addTodo('First')
      66 |       await addTodo('Second')
    > 67 |       expect(elementText('todo-0')).toMatch('First')
         |                                     ^
      68 |       expect(elementText('todo-1')).toMatch('Second')
      69 |     })
      70 |     it('items can be marked as done by clicking an element before the item.', async () => {

      at Object.it (tests/unit/components/Todo.spec.js:67:37)

This is caused by the fact that we use the same instance of the store in every test. Instead, we want a fresh store at the start of each test. We can fix this by altering the store index file and adding a createStore function which we export:

// src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import todo from '@/store/todo'

Vue.use(Vuex)

export function createStore () {
  return new Vuex.Store({
    state: {},
    mutations: {},
    actions: {},
    modules: {
      todo
    }
  })
}

export default createStore()

Now we can use this function to create a new store for each test:

// src/components/Todo.vue

import { createStore } from '@/store'

const localVue = createLocalVue()
localVue.use(Vuex)
let store

describe('The Todo.vue component', () => {
  beforeEach(() => {
    store = createStore()
  })
  ...
})

All unit tests pass again! We successfully moved the state from the component to the vuex store.

The code so far can be found on github

The missing tests

Although we successfully refactored the code, we still have the problem that the store can be regarded as an input for, and output of the component. In the existing tests we do not take this into account. There is a even a bigger problem: strictly speaking the test looks more like an integration test than a unit test right now.

Question is: is this a problem? I don't think so. I even think it is an advantage! We got ourselves our first integration test.

Of course we are not done yet. Remember that the reason we did the refactoring is that we wanted to have the form for adding a todo separate from the display. There is absolutely no reason for the refactoring we just did if we stopped here. In the next episode we will start extracting the form and the display components. We will need to write the unit tests in which the store is either an input to or an output of the new component.

Discussion (0)