introduce

Vue-test-utils is the official vue. js unit Test utility library. It provides a series of apis to make it easy to write unit tests for Vue applications.

There are many mainstream unit Test runners, such as Jest, Mocha, Karma, etc. There are tutorials for these in the Vue-test-utils documentation. Here we will only show examples of vue-test-utils + Jest.

Jest is a testing framework developed by Facebook. Vue describes it as the most fully functional test runner. The configuration required is minimal, JSDOM is installed by default, assertions are built in, and the command line user experience is excellent. However, you need a preprocessor that can import single-file components into the test. We have created the Vue-jest preprocessor to handle the most common single file component features, but it is still not 100% functional for Vue-Loader.

Environment configuration

When creating a new project using scaffolding vue-CLI, if Unit Testing is selected and Jest is selected as the test runner, the environment for Unit Testing will be automatically configured once the project is created. Use vue-test-utils and Jest apis directly to write Test cases.

However, the unit test function was not selected at the beginning of the new project. If it needs to be added later, there are two options:

The first configuration:

Adding an unit-jest plug-in directly to your project will automatically install and configure the required dependencies.

vue add @vue/unit-jest
Copy the code

Second configuration:

This configuration will be a little more troublesome, the following is the specific operation steps.

Install dependencies

  • Install Jest and Vue Test Utils

    npm install --save-dev jest @vue/test-utils
    Copy the code
  • Install Babel-jest, Vue-jest, and Babel-core of version 7.0.0-Bridge.0

    NPM install --save-dev babel-jest vue-jest [email protected]Copy the code
  • Install the jest – serializer – vue

    npm install --save-dev jest-serializer-vue
    Copy the code

configurationJest

Jest configuration can be configured in package.json; You can also create a new file, jest.config.js, and place it in the project root directory. Here I chose to configure it in jest.config.js:

module.exports = {
    moduleFileExtensions: [
        'js'.'vue'].transform: {
        '^.+\\.vue$': '<rootDir>/node_modules/vue-jest'.'^.+\\.js$': '<rootDir>/node_modules/babel-jest'
    },
    moduleNameMapper: {
        '^ @ / (. *) $': '<rootDir>/src/$1'
    },
    snapshotSerializers: [
        'jest-serializer-vue'].testMatch: ['**/__tests__/**/*.spec.js'].transformIgnorePatterns: ['<rootDir>/node_modules/']}Copy the code

The configuration items are as follows:

  • moduleFileExtensionstellJestFile suffix to be matched
  • transformMatch to the.vueFor filesvue-jestProcessing, matching to.jsFor filesbabel-jestTo deal with
  • moduleNameMapperTo deal withwebpackFor example, will@said/srcdirectory
  • snapshotSerializersSerialize the saved snapshot test results to make them aesthetically pleasing
  • testMatchWhich files are matched for testing
  • transformIgnorePatternsThe directory that is not matched

configurationpackage.json

Write a command script to execute the test:

{
    "script": {
        "test": "jest"}}Copy the code

The first test case

To ensure the consistency of the environment, let’s demonstrate the steps from creating the project.

withvue-cliCreate a project

Currently I am using vue-CLI version 3.10.0. To start creating a project:

vue create first-vue-jest
Copy the code

Select Manually Select Features to configure the manual selection function:

Vue CLI v3.10.0 ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ Update available: 4.0.4 │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘? Please pick a preset: Vue-CLI3 (Vue-router, node-sass, Babel, ESlint) default (Babel, eslint) ❯ Manually select FeaturesCopy the code

Check Babel, Unit Testing:

? Check the features needed foryour project: ◉ Babel Insights into TypeScript insights into Progressive Web App (PWA) Support insights into Router insights into CSS pre-processors insights into Linter/Formatter ◉ Unit Testing insights into TestingCopy the code

Choose Jest:

? Pick a unit testing solution:
  Mocha + Chai
❯ Jest
Copy the code

Select In Dedicated Config Files to configure the configuration information In the corresponding config file:

? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? (Use arrow keys)
❯ In dedicated config files
  In package.json
Copy the code

Enter n, do not save the preset:

? Save this as a preset for future projects? (y/N) n
Copy the code

After the project is created, the configuration information of some files is as follows:

babel.config.js:

module.exports = {
    presets: [
        '@vue/cli-plugin-babel/preset']}Copy the code

Jest.config. js, the configuration of this file is the default plug-in, you can change it to the same configuration in jEST as mentioned above.

module.exports = {
    preset: '@vue/cli-plugin-unit-jest'
}
Copy the code

package.json:

{
    "name": "first-vue-jest"."version": "0.1.0 from"."private": true."scripts": {
        "serve": "vue-cli-service serve"."build": "vue-cli-service build"."test:unit": "vue-cli-service test:unit"
    },
    "dependencies": {
        "core-js": "^ 3.1.2." "."vue": "^ 2.6.10"
    },
    "devDependencies": {
        "@vue/cli-plugin-babel": "^ 4.0.0"."@vue/cli-plugin-unit-jest": "^ 4.0.0"."@vue/cli-service": "^ 4.0.0"."@vue/test-utils": "29 1.0.0 - beta."."vue-template-compiler": "^ 2.6.10"}}Copy the code

Executing a Test command

When the project created with the above steps is complete, we can see that there is a test:unit in the scripts item of package.json, execute it:

cd first-vue-jest
npm run test:unit
Copy the code

Then the terminal will see the output. PASS means that the test case passed. This is the official unit test example. So let’s write something of our own.

Implement a ToDoList

Looking at the prototype above, there are several specific requirements:

  • Enter what you want to do in the input box on the right side of the header. When you hit Enter, the content is displayed in the to do list and the input box is cleared
  • When the input field is empty, hit Enter and do not change anything
  • The to-be-completed list can be edited, but the completed list cannot be edited
  • Each list item has a delete button on the right, using-Is clicked to delete the item
  • The to-do list has buttons marked completed, withSquare rootIs clicked to move the current item to the completed list
  • The completed list has buttons marked as unfinished, withxNumber indicates that the current item moves to the incomplete list after clicking
  • The list number increases from 1
  • When the backlog list is empty, the backlog is not displayed
  • When the completed list is empty, the completed word is not displayed

Write the top page first

Delete helloworld.vue and testexample.spec.js generated when creating the project before writing the page. Also modify the app. vue file to introduce the ToDoList component:

<template>
    <div id="app">
        <ToDoList></ToDoList>
    </div>
</template>

<script>
import ToDoList from './components/ToDoList'

export default {
    components: {
        ToDoList
    }
}
</script>
Copy the code

Create a new file todolist.vue under SRC /compoents.

<template>
    <div class="todolist">
        <header>
            <h5>ToDoList</h5>
            <input class="to-do-text" 
                v-model="toDoText" 
                @keyup.enter="enterText" 
                placeholder="Enter what you plan to do."/>
        </header>
        <h4 v-show="toDoList.length > 0">pending</h4>
        <ul class="wait-to-do">
            <li v-for="(item, index) in toDoList" :keys="item">
                <p>
                    <i>{{index + 1}}</i>
                    <input :value="item" @blur="setValue(index, $event)" type="text" />
                </p>
                <p>
                    <span class="move" @click="removeToComplete(item, index)">Square root</span>
                    <span class="del" @click="deleteWait(index)">-</span>
                </p>
            </li>
        </ul>
        <h4 v-show="completedList.length > 0">Has been completed</h4>
        <ul class="has-completed">
            <li v-for="(item, index) in completedList" :keys="item">
                <p>
                    <i>{{index + 1}}</i>
                    <input :value="item" disabled="true" type="text" />
                </p>
                <p>
                    <span class="move" @click="removeToWait(item, index)">x</span>
                    <span class="del" @click="deleteComplete(index)">-</span>
                </p>
            </li>
        </ul>
    </div>
</template>
Copy the code
<script>
export default {
    data() {
        return {
            toDoText: ' '.toDoList: [].completedList: []}},methods: {
        setValue(index, e) {
            this.toDoList.splice(index, 1, e.target.value)
        },
        removeToComplete(item, index) {
            this.completedList.splice(this.completedList.length, 0, item)
            this.toDoList.splice(index, 1)
        },
        removeToWait(item, index) {
            this.toDoList.splice(this.toDoList.length, 0, item)
            this.completedList.splice(index, 1)
        },
        enterText() {
            if (this.toDoText.trim().length > 0) {
                this.toDoList.splice(this.toDoList.length, 0.this.toDoText)
                this.toDoText = ' '
            }
        },
        deleteWait(index) {
            this.toDoList.splice(index, 1)
        },
        deleteComplete(index) {
            this.completedList.splice(index, 1)}}};</script>
Copy the code

When the page is written and the requirements on the prototype are roughly developed, the page will look like this:

Modifying directory Configuration

The next step is to write the unit test file. Before writing the unit test file, change the directory of the test file to __tests__, and change the jest.config.js to the following configuration. Note that testMatch has been changed to match all the.js files in __tests__.

module.exports = {
    moduleFileExtensions: [
        'js'.'vue'].transform: {
        '^.+\\.vue$': '<rootDir>/node_modules/vue-jest'.'^.+\\.js$': '<rootDir>/node_modules/babel-jest'
    },
    moduleNameMapper: {
        '^ @ / (. *) $': '<rootDir>/src/$1'
    },
    snapshotSerializers: [
        'jest-serializer-vue'].testMatch: ['**/__tests__/**/*.spec.js'].transformIgnorePatterns: ['<rootDir>/node_modules/']}Copy the code

Writing test files

In the __tests__/unit/ directory, create a new file todolist.spec.js. We are supposed to test a vue file, so its unit test file is conventionally named as *.spec.js or *.test.js.

import { shallowMount } from '@vue/test-utils'
import ToDoList from '@/components/ToDoList'

describe('test ToDoList', () => {
    it('Input box starts with an empty string', () = > {const wrapper = shallowMount(ToDoList)
        expect(wrapper.vm.toDoText).toBe(' ')})})Copy the code

The above test file briefly explains:

  • shallowMountWill be created to contain the mounted and renderedVueThe component’sWrapper, only stubs the current component and does not contain child components.
  • describe(name, fn)This is defining a test suite,test ToDoListIs the name of the test suite,fnIs a concrete executable function
  • it(name, fn)It’s a test case,The input box starts with an empty stringIs the name of the test case,fnAre concrete executable functions; Multiple test cases can be protected in a test suite.
  • expectJestBuilt-in assertion style. There are other assertion styles in the industry such asShould,AssertAnd so on.
  • toBeJestProvides the assertion method, more can be toJest ExpectSee the usage.
it('The to-do list should start with an empty array.', () = > {const wrapper = shallowMount(ToDoList)
    expect(wrapper.vm.toDoList.length).toBe(0)
})

it('Completed lists should start with an empty array', () = > {const wrapper = shallowMount(ToDoList)
    expect(wrapper.vm.completedList).toEqual([])
})
Copy the code

Pending and completed lists are lists, so the fields that hold the data must be of type Array. An empty list is an empty Array. If the second test case is changed to:

expect(wrapper.vm.completedList).toBe([])
Copy the code

An error will be reported because the toBe method internally calls object.is (value1, value2) to compare whether two values are equal, which is not the same as == or ===. Obviously object.is ([], []) returns false.

it('toDoText should change when the input field value changes.', () = > {const wrapper = shallowMount(ToDoList)
    wrapper.find('.to-do-text').setValue('To accompany my mother to the supermarket in the evening')
    expect(wrapper.vm.toDoText).toBe('To accompany my mother to the supermarket in the evening')
})
it('There is no value in the input box. When I type enter, nothing changes.', () = > {const wrapper = shallowMount(ToDoList)
    const length = wrapper.vm.toDoList.length
    const input = wrapper.find('.to-do-text')
    input.setValue(' ')
    input.trigger('keyup.enter')
    expect(wrapper.vm.toDoList.length).toBe(length)
})
it('The input field has a value. When you enter enter, the list will be added and the input field will be cleared.', () = > {const wrapper = shallowMount(ToDoList)
    const length = wrapper.vm.toDoList.length
    const input = wrapper.find('.to-do-text')
    input.setValue('Go out to dinner')
    input.trigger('keyup.enter')
    expect(wrapper.vm.toDoList.length).toBe(length + 1)
    expect(wrapper.vm.toDoText).toBe(' ')})Copy the code
  • setValueYou can set the value of a text control and update itv-modelBound data.
  • .to-do-textIs aCSSThe selector;Vue-Test-UtilsprovidesfindMethod to return one by looking at the selectorWrapper; The selector could beCSSSelector, can beVueA component can also be an object that contains the component’snamerefProperty, for example:wrapper.find({ name: 'my-button' })
  • wrapper.vmIs aVueInstance, onlyVueComponent wrappers are availablevmThis property; throughwrapper.vmAccess to allVueThe properties and methods of the instance. Such as:wrapper.vm.$data,wrapper.vm.$nextTick().
  • triggerMethod can be used to trigger aDOMEvent, the events that are fired here are synchronous, so you don’t have to put an assertion in$nextTick()To carry out; It also supports passing in an object. When an event is captured, the properties of the passed object can be retrieved. It could be written like this:wrapper.trigger('click', {name: "bubuzou.com"})
it('The toDoList array is updated after editing.', () = > {const wrapper = shallowMount(ToDoList)
    wrapper.setData({toDoList: ['Run for half an hour']})
    wrapper.find('.wait-to-do li').find('input').setValue('Run around the park three times') 
    wrapper.find('.wait-to-do li').find('input').trigger('blur') 
    expect(wrapper.vm.toDoList[0]).toBe('Run around the park three times')})Copy the code

SetData is used to set an initial value for toDoList to render a list item. Then find the list item and set its value with setValue, simulating editing; The list item input field is bound with :value=”item”, so setValue cannot trigger an update; Updates to the toDoList value can only be triggered using the trigger.

it('Click to delete the toDoList list while updating the toDoList array', () = > {const wrapper = shallowMount(ToDoList)
    wrapper.setData({toDoList: ['Read an hour before bed']})
    expect(wrapper.vm.toDoList.length).toBe(1)
    wrapper.find('.wait-to-do li').find('.del').trigger('click')
    expect(wrapper.vm.toDoList.length).toBe(0)
})
it('Click the completed button for an item in the backlog, and the data will be updated.', () = > {const wrapper = shallowMount(ToDoList)
    wrapper.setData({toDoList: ['Eat an apple after lunch']})
    expect(wrapper.vm.toDoList.length).toBe(1)
    expect(wrapper.vm.completedList.length).toBe(0)
    wrapper.find('.wait-to-do li').find('.move').trigger('click')
    expect(wrapper.vm.toDoList.length).toBe(0)
    expect(wrapper.vm.completedList.length).toBe(1)
})
it('Click the unfinished button for an item in the completed list to update the data.', () = > {const wrapper = shallowMount(ToDoList)
    wrapper.setData({completedList: ['Sang a song']})
    expect(wrapper.vm.toDoList.length).toBe(0)
    expect(wrapper.vm.completedList.length).toBe(1)
    wrapper.find('.has-completed li').find('.move').trigger('click')
    expect(wrapper.vm.toDoList.length).toBe(1)
    expect(wrapper.vm.completedList.length).toBe(0)
})
it('List sequence increments from 1', () = > {const wrapper = shallowMount(ToDoList)
    wrapper.setData({toDoList: ['Do your homework in the morning'.'Go shopping in the afternoon']})
    expect(wrapper.vm.toDoList.length).toBe(2)
    expect(wrapper.find('.wait-to-do').html()).toMatch('<i>1</i>')
    expect(wrapper.find('.wait-to-do').html()).toMatch('<i>2</i>')
})
it('Do not display the backlog when the backlog list is empty', () = > {const wrapper = shallowMount(ToDoList)
    wrapper.setData({toDoList: []})
    expect(wrapper.find('h4').isVisible()).toBeFalsy()
    wrapper.setData({toDoList: ['Climb the North Mountain tomorrow.']})
    expect(wrapper.find('h4').isVisible()).toBeTruthy()
})
Copy the code

You can write more than one Expect in a test case to ensure that assertions are accurate.

Asynchronous test

Finally, to simulate asynchronous testing, we added a requirement that the page load request the remote backlog data. Create a new __mocks__ directory at the project root and create axios.js:

const toToList = {
    success: true.data: ['Reading in the library in the morning'.'Go down and go shopping']}export const get = (url) = > {
    if (url === 'toToList.json') {
        return new Promise((resolve, reject) = > {
            if (toToList.success) {
                resolve(toToList)
            } else {
                reject(new Error())}}Copy the code

Modify todolist. vue, import axios and add Mounted:

<script>
import * as axios from '.. /.. /__mocks__/axios'
export default {
    mounted () {
        axios.get('toToList.json').then(res= > {
            this.toDoList = res.data
        }).catch(err= >{})}};</script>

Copy the code

The test case is written as:

it('When the page is mounted to request data, it should return 2 pieces of data when the request succeeds.', (done) => {
    wrapper.vm.$nextTick((a)= > {
        expect(wrapper.vm.toDoList.length).toBe(2)
        done()
    })
})
Copy the code

For asynchronous code, write the assertion in wrapper.vm.$nextTick() and manually call done().

Configuring Test Coverage

If we look at what the coverage is, we need to configure the test coverage. Add configuration in jest.config.js:

collectCoverage: true.collectCoverageFrom: ["**/*.{js,vue}".! "" **/node_modules/**"].Copy the code

Add a configuration in the package.json scripts:

"test:cov": "vue-cli-service test:unit --coverage"
Copy the code

Then we run: NPM run test:cov on the terminal. The result is as follows:

Running the test coverage name will generate the coverage directory in the project root directory, and the browser will open the index. HTML inside: