In this paper, starting from the Vue application strategy and practice of unit testing 04 – Vuex unit test | Lv Liqing blog

Welcome to zhihu column — the reverse attack of the front end (where JavaScript can be, JavaScript will be.)

Welcome to my blog, Zhihu, GitHub, Nuggets.


Objective of this paper

2.2 How to test Vuex Store in the unit test of Vue application? How do I test interaction with Vue components?

// Given a developer with basic UT knowledge and Vue component unit testing experience 🚶 // When he 🚶 reads and practices the Vuex unit testing section of this article // Then he can understand Vuex concepts in greater depth and know`Redux-like`Can he reasonably test mutation in the Vuex store, business logic in the getter, and asynchronous actions? Can he test how the component correctly reads state in the store and dispatch actionsCopy the code

How to understand the Vuex pattern?

Lessons from Vuex

Vuex is a state management mode developed specifically for vue.js applications. It uses centralized storage to manage the state of all components of an application and rules to ensure that the state changes in a predictable way.

The ancient people said that “reading history makes one wise”, learning history is to better move forward, to be able to understand the present and see the future. Let’s take a look at the history of Vuex. Vuex borrows from Redux, which was originally conceived in Flux, an application architecture designed by Facebook for its application. The Flux pattern seems to have found a new home in JavaScript applications, but it just borrowed from domain-driven design (DDD) and command-query separation of responsibilities (CQRS).

CQRS and Flux architecture

One of the most common ways to describe Flux is to compare it to the Model-View-Controller (MVC) architecture.

In MVC, a Model can be read by multiple Views and updated by multiple Controllers. In large applications, a single Model can cause multiple Views to notify Controllers and possibly trigger more Model updates, which can lead to complex results.

Flux, as well as Vuex, tries to solve this complexity by enforcing one-way data flow. In this architecture, Views query Stores (rather than Models), and user interactions trigger Actions, which are submitted to a centralized Dispatcher. When Actions are delivered, Stores will update themselves and notify Views of the changes. Changes in these stores further force Views to query for new data.

The biggest difference between MVC and Flux is the separation of query and update. In MVC, the Model can also be updated by the Controller and queried by the View. In Flux, the View gets read-only data from the Store. Stores can only be updated by Actions, which affects the Store itself rather than the read-only data.

The model described above is very close to the CQRS first proposed by Greg Young:

  1. If a method modifies the state of the object, it is onecommand(command) and must not return a value.
  2. If a method returns some value, it’s onequery(query), and must not change the state.

The basic idea behind Vuex

Therefore, Vuex extracts the shared state “state” of components and manages them uniformly in a singleton mode of global “Store”. In this mode, our tree of components forms a giant “view” where any component can pick up state or trigger behavior no matter where in the tree.

In addition, there are many benefits to isolated state management, as well as enforcing compliance with certain rules:

  1. Vuex’s state storage is reactive. When the Vue component reads the state from the Store, if the state in the store changes, the corresponding component is updated efficiently accordingly. That’s what CQRS isqueryAn implementation of a query.
  2. You can’t just change the state in the store. The only way to change state in a Store is explicitly(commit) mutation, which makes it easy to track each state change. That’s what CQRS iscommandAn implementation of (a command).

How do I unit test Vuex

Thanks to Vuex’s ability to isolate the shared state of Vue applications, our code becomes more structured and easier to maintain. Mutation, action, and getters in Vuex are placed in a proper place with different responsibilities. This also makes it much easier to unit test them.

Mutations test

Mutation is easy to test because they are simply functions that depend entirely on parameters. The simplest mutation test only saves slices of data in one-to-one correspondence. This mutation does not need test coverage because it is basically guaranteed by simple architecture and logic and does not need to read test cases to understand it. However, a relatively complex mutation with test value may have performed merging and de-duplication operations as well as preserving data.

// count.js
const state = { ... }
const actions = { ... }
export const mutations = {
  increment: state= > state.count++
}
// count.test.js
import { mutations } from './store'

/ / deconstruction ` mutations `
const { increment } = mutations

describe('mutations', () => {
  it('INCREMENT', () = > {// Simulate the state
    const state = { count: 0 }
    / / application mutation
    increment(state)
    // Assert the result
    expect(state.count).toEqual(1)})})Copy the code

The actions test

Actions are a little trickier to deal with because they may need to call external apis. When testing actions, we need to add a mocking service layer — for example, we can abstract API calls into services and mock them in test files to respond to the expected API calls.

// product.js
import shop from '.. /api/shop'

export const actions = {
  getAllProducts({ commit }) {
    commit('REQUEST_PRODUCTS')
    shop.getProducts(products= > {
      commit('RECEIVE_PRODUCTS', products)
    })
  }
}
Copy the code
// product.test.js
jest.mock('.. /api/shop', () = > ({getProducts: jest.fn((a)= > /* mocked response */),
}))

describe('actions', () => {
  it('getAllProducts', () = > {const commit = jest.spy()
    const state = {}
    
    actions.getAllProducts({ commit, state })
    
    expect(commit.args).toEqual([
      ['REQUEST_PRODUCTS'],
      ['RECEIVE_PRODUCTS', { /* mocked response */}]])})})Copy the code

Getters test

Testing for getters is as straightforward as mutation. Getters is also more logical, and it is a pure function, which enjoys the same treatment as the Mutations test: pure input and output, and easy test preparation. Let’s look at a slightly simpler getters test case:

// product.js
export const getters = {
  filteredProducts (state, { filterCategory }) {
    return state.products.filter(product= > {
      return product.category === filterCategory
    })
  }
}
Copy the code
// product.test.js
import { expect } from 'chai'
import { getters } from './getters'

describe('getters', () => {
  it('filteredProducts', () = > {// Simulate the state
    const state = {
      products: [{id: 1.title: 'Apple'.category: 'fruit' },
        { id: 2.title: 'Orange'.category: 'fruit' },
        { id: 3.title: 'Carrot'.category: 'vegetable'}}]/ / simulate the getter
    const filterCategory = 'fruit'

    // Get the result of the getter
    const result = getters.filteredProducts(state, { filterCategory })

    // Assert the result
    expect(result).to.deep.equal([
      { id: 1.title: 'Apple'.category: 'fruit' },
      { id: 2.title: 'Orange'.category: 'fruit'})})})Copy the code

Interaction between Vue components and the Vuex Store

Now that we have covered the basics of Vuex unit testing, how can we test the interaction between Vue components that need to read state from the Vuex Store or send action to change state from the Store? Let’s talk about how to Test Vuex in Vue components using Vue Test Utils.

From a unit testing perspective, we don’t need to know what the Vuex Store looks like when we test Vue components (units). We just need to know what the actions in the Vuex Store will look like when they fire and what the expected behavior is when they fire.

<template>
  <div class="app">
    <div class="price">amount: ${{$store.state.price}}</div>
    <button @click="actionClick()">Buy</button>
  </div>
</template>

<script>
import { mapActions } from 'vuex'
export default {
  methods: {
    ...mapActions([
      'actionClick']),}}</script>
Copy the code

During unit testing, the shallowMount method accepts a mount option that can be used to pass a fake store to the Vue component. We can then use Jest to simulate an action’s behavior and pass it to the Store, and actionClick, a fake function, allows us to assert that the action was called. So when we test the action, we can only care about the action firing, and we don’t need to care about what happens to the store after the action firing, because Vuex unit tests cover the code logic.

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

const fakeStore = new Vuex.Store({
  state: {},
  actions: {
    actionClick: jest.fn()
  }
})

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

it('Call' actionClick 'action when button is clicked', () = > {const wrapper = shallowMount(Actions, { store: fakeStore, localVue })
    wrapper.find('button').trigger('click')
    expect(actions.actionClick).toHaveBeenCalled()
})
Copy the code

Note that here we are passing the Vuex store to a localVue, not to the underlying Vue constructor. This is because we don’t want to affect the global Vue constructor, and using vue.use (Vuex) directly would cause the Vue prototype to add the $store attribute and affect other unit tests. LocalVue, on the other hand, is a scoped Vue constructor that can be modified at will.

Of course, we can mock anything in the Vuex Store except actions, such as state or getters:

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

const fakeStore = new Vuex.Store({
  state: {
    price: '998'
  },
  getters: {
    clicks: (a)= > 2.inputValue: (a)= > 'input'}})const localVue = createLocalVue()
localVue.use(Vuex)

it('Render price and' state.inputValue 'in app', () = > {const wrapper = shallowMount(Components, { store: fakeStore, localVue })
  expect(wrapper.find('p').text()).toBe('input')  
  expect(wrapper.find('.price').text()).stringContaining('$998')})Copy the code

To summarize

In short, don’t test Vue components to interact with the Vuex Store by introducing a real store, it’s no longer a unit test, Remember the Social or Solitary Tests unit we discussed in the second unit test basics article? The “State management mode” of Vuex and other Redux-like architectures in front-end applications has split and isolated View layer and State data layer as reasonably as possible, so the unit test only needs to test Vue and Vuex respectively. This ensures that Vue components and data flows work as expected.

To be continued…

## Unit testing basics

  • [X] ### The implications of unit Testing and automation
  • [x] ### why Jest
  • [x] ### Jest
  • [x] ### How do I test asynchronous code?

## Vue unit test

  • [x] ### Vue components render
  • [x] ### Wrapper find()Methods and selectors
  • [x] ### test UI component interaction behavior

## Vuex unit test

  • # # # CQRS and [x]Redux-likearchitecture
  • [x] ### How to unit test Vuex
  • [x] ### Interaction between Vue components and Vuex Store

## Vue applies the test policy

  • [] ### unit testing features and locations
  • [] ### unit test concerns
  • [] ### apply the test strategy of the test